【MBTI診断】16タイプ別・フリーランスに向いてる仕事/働き方
- コラム
- フリーランス/個人事業主
- 働き方
クリーンなコードと可読性の高さが高く評価されているPythonは、今や駆け出しからベテランまで幅広いエンジニアから支持を集めており、機械学習やデータ解析をはじめとした幅広い分野で利用されています。
今回は、Pythonを勉強中の初心者が失敗しがち、間違えがちなポイントを6つピックアップしてみました。
目次
Pythonの文法は他言語と比べてやや独特な部分があります。
例えば以下のようなポイント。
他言語と同じつもりで書いていると、時に思わぬエラーが発生するでしょう。
このように、確かに癖のあるPyrthonの文法ですが、慣れればとても可読性の高い言語です。特に内包表記は、他言語であれば複数行に渡ってコードを書かなければいけない場合が多いですが、Pythonであればひとまとめに分かりやすく記述できます。
例えば以下のような感じです。
# 0〜99までの17の倍数のリストを作成する result = [] for i in range(100): if i%17 == 0: result.append(i) # 変数result:[0, 17, 34, 51, 68, 85] # 上記コードを内包表記で書くと… result = [i for i in range(100) if i%17 == 0] # 変数result:[0, 17, 34, 51, 68, 85]
リストや辞書(他言語における連想配列)を、さまざまな条件を指定してコンパクトに作成できるので便利です。
Pythonではクラス定義の時、以下のようにインスタンスメソッドを書いてはいけません。
class Foo: def exe(): print("foo!!") f = Foo() # 実行するとエラーがでる f.exe() # TypeError: exe() takes 0 positional arguments but 1 was given
他言語の経験があっても、Pythonに慣れていなければ、一見このコードのどこに問題があるか分からないかもしれません。
このエラーでは「exe()は0個の引数をとるはずだが、1個の引数が与えられている」と指摘されていますが、コード中にはf.exe()と書いてあるのみで、引数はひとつも与えていないはずです。
Pythonではクラスメソッドやインスタンスメソッドを呼ぶ際、第一引数に「自身のオブジェクトを示す値(一般にselfと呼ばれます)」が自動的に渡されます。それこそがエラーの原因です。
試しに以下のコードを実行してみましょう。
class Hoo: def exe(x): print(x) h = Hoo() h.exe() # 出力:<__main__.Hoo object at 0x10fd1cb00> # ▲ 第一引数xには、自身(Hooオブジェクト)が格納されている
第一引数を出力するHoo.exe()メソッドを実行すると、Hooオブジェクト自身が出力されました。メソッド実行時にはひとつも与えていなかったはずの引数に、自動的に自分自身を示すオブジェクトが注入されたのです。
このためPythonでクラスメソッド・インスタンスメソッドを定義する場合には、あらかじめ第一引数に自分自身のオブジェクトを意味する変数selfを指定するのが一般的です。
class Human: def __init__(self, name): self.name = name def hello(self, your_name): print(f"Hello, {your_name}! My name is {self.name}.") sig = Human("sig_Left") # selfが自動補完 sig.hello("Sato") # ここでもselfが補完 # 出力:Hello, Sato! My name is sig_Left.
「文中から正規表現で特定の文字列を抽出したい」というとき、以下のコードを書くとエラーになります。
re.match("category\/(.+?)\/", "https://foo.com/category/books/murakami").group(1) # AttributeError: 'NoneType' object has no attribute 'group'
上記コードでは正規表現がどの文字列にもヒットしていません。正規表現で「category」から「/」までの間の文字列を抜き出すように書いているはずですが、なぜでしょうか。
答えは、Pythonのmatch()メソッドは「文字列の先頭から」正規表現がヒットするかを確認するからです。
Pythonでは「前方一致」と「部分一致」の正規表現操作がメソッドごとに分かれています。今回のように文字列中の一部を抜き出したい場合には、部分一致用のsearch()メソッドを使うとよいでしょう。
re.search("category\/(.+?)\/", "https://foo.com/category/books/murakami").group(1) # 取得した文字列:'books'
Pythonを扱っていて、誰もが一度は次のようなエラーに遭遇したことがあるでしょう。
g = "グローバル" def foo(): print(f"before: {g}") g = "ローカル" print(f"after: {g}") foo() # UnboundLocalError: local variable 'g' referenced before assignment
エラーには「ローカル変数”g”が代入前に参照されている」とあります。
これだけ見ると「ああ、Pythonは関数内からグローバル変数を参照できないのか」と思うかもしれませんが、事態はそう単純ではありません。
次のコードをみてください。
g = "グローバル" def hoo(): print(f"before: {g}") hoo() # 出力:before: グローバル
なんと、上記コードでは関数内からグローバル変数を参照できてしまっています。ひとつ前のコードでは、変数gはローカル変数として扱われていたはずですが……どういうことでしょう。
答えは「関数内の代入の有無」にあります。Pythonでは、変数のスコープが代入の有無で変わるのです。
上記スコープは、代入のタイミングによらず、ともかく代入の記述があるかどうかで決まります。「代入された時点でグローバル変数からローカル変数に変わる」のではなく、「代入があるなら問答無用でローカル変数になる」のです。
g = "グローバル" def foo(): """ 代入があるので変数gはローカルスコープ """ print(f"before: {g}") # ローカル変数gはこの行ではまだ存在しておらず、エラーになる g = "書き換え" print(f"after: {g}") def hoo(): """ 代入がないので変数gはグローバルスコープ """ print(f"before: {g}")
なお、関数内からグローバル変数の値を書き換えたい場合、関数内で「その変数がグローバルスコープの変数であること」をglobalキーワードで明示的に示す必要があります。
g = "グローバル" def foo(): global g # グローバル変数であることの宣言 print(f"before: {g}") # ローカル変数gはこの行ではまだ存在しておらず、エラーになる g = "書き換え" print(f"after: {g}") foo() # 以下出力: # before: グローバル # after: 書き換え
次のようなファイル(number.txt)があるとします。
1 12 123
このファイルを、Pythonで1行ずつ読み取ってみましょう。
with open("number.txt") as f: line1 = f.readline() line2 = f.readline() line3 = f.readline() print(f"1行目: {line1}") print(f"2行目: {line2}") print(f"3行目: {line3}") # 以下出力: # 1行目: 1 # # 2行目: 12 # # 3行目: 123
特別変わったこともなく、ただファイルの内容をそのまま取れているようにも見えます。
しかしよく見てください。print()で出力した内容には、1行目と2行目、2行目と3行目との間に空の行が挟まれていませんか。
試しに、上記コードに1行ごとの文字数の確認を含めてみましょう。
with open("number.txt") as f: line1 = f.readline() line2 = f.readline() line3 = f.readline() print(f"1行目: {line1}, 文字数: {len(line1)}") print(f"2行目: {line2}, 文字数: {len(line2)}") print(f"3行目: {line3}, 文字数: {len(line3)}") # 以下出力: # 1行目: 1 # , 文字数: 2 # 2行目: 12 # , 文字数: 3 # 3行目: 123, 文字数: 3
1行目には2文字、2行目には3文字、3行目には3文字という結果になりました。一見すると1行目は1文字、2行目には2文字のように見えますが……これはなぜでしょうか。
答えは「改行コード」が含まれているためです。
1行目と2行目の末尾には1文字分の改行コードがあり、3行目はファイルの終端のため改行コードがありません。つまりreadline()は、この改行コードごと文字列を取得してしまうのです。
メソッド自体が「1行ずつファイルの文字列を取得する」という挙動のものなのだから、「まさかその行の区切りである改行コードまで取得するとは……」と驚く方も多いでしょう。筆者も最初はこれを知らず「ファイルに書かれたURLを取得してアクセスしようとしたら、末尾に改行コードが混じっていたせいで存在しないURLと見なされた」という経験があります。
改行が含まれることを想定して、あらかじめ改行コードがあれば文字を削除するよう実装するのが良いでしょう。
引数で指定した要素を持った配列を作成する関数として、以下のようなコードを書いたとしましょう。
def add(*e, list=[]): """ 可変引数eを先頭から並べた配列を出力する. またキーワード引数listに配列を指定すると、その配列に可変引数eを追加する """ for element in e: list.append(element) print(list)
一見とくに問題なさそうな関数ですが、この関数には致命的な問題があります。この関数を使って、配列を出力してみましょう。
# 問題なし(キーワード引数listを指定) add(1, 2, 3, list=[10, 9]) # 出力:[10, 9, 1, 2, 3] add("a", "b", list=["c", "d"]) # 出力:['c', 'd', 'a', 'b'] # 問題あり(キーワード引数listはデフォルト) # ※デフォルトの配列が同じオブジェクトとして使い回される add(1, 2) # 出力:[1, 2] add(3, 4) # 出力:[1, 2, 3, 4] add(5, 6) # 出力:[1, 2, 3, 4, 5, 6]
キーワード引数listを指定せず、デフォルトの配列を使用した場合の結果が……なにやらおかしいですね。関数呼び出しの際、前回の呼び出しで追加した要素がそのままデフォルト配列内に残っているようです。
これはPythonにおいて、デフォルトの引数として指定した配列や辞書型オブジェクトが、関数呼び出しの度に同じオブジェクトとして使い回されていることが原因です。
呼び出しの度にリセットされた新しい配列を使いたいのであれば、デフォルト引数ではなく関数内の変数として毎回新しいオブジェクトを作成しましょう。
Pythonは他言語と比べて、クリーンかつ可読性の高いコードを書けます。極力シンプルに書けるよう工夫を凝らされたその文法には、ところどころ癖があります。しかし癖さえ抑えれば、とても扱いやすく、短期間でさまざまな実装をすることができます。
手を動かしながら学習すれば、Python特有の文法もすぐに身に付けられるでしょう。コードを書きながら、テンポよくPythonの技術を身につけてください!
こちらもおすすめ!▼
2024年 東京都内の優良プログラミングスクール20選
Workship MAGAZINE
オンラインのプログラミング学習サービス27選
Workship MAGAZINE