Python初心者が失敗しがち/間違えがちなこと6選

Python初心者あるある

クリーンなコードと可読性の高さが高く評価されているPythonは、今や駆け出しからベテランまで幅広いエンジニアから支持を集めており、機械学習やデータ解析をはじめとした幅広い分野で利用されています。

今回は、Pythonを勉強中の初心者が失敗しがち、間違えがちなポイントを6つピックアップしてみました。

1. 慣れないうちは癖のある文法でエラーを出しがち

Pythonの文法は他言語と比べてやや独特な部分があります。

例えば以下のようなポイント。

  • 文末を示すセミコロンがなく、代わりにインデントを整えることで文の区切りを区別する
  • nullではなくNone
  • trueではなくTrue
  • new Class()ではなくClass()でインスタンスを作成できる

他言語と同じつもりで書いていると、時に思わぬエラーが発生するでしょう。

このように、確かに癖のある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]

リストや辞書(他言語における連想配列)を、さまざまな条件を指定してコンパクトに作成できるので便利です。

2. インスタンスメソッドに第一引数selfを定義せず、エラーを出してしまう

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.

3. 正規表現操作のre.match()メソッドで文字列中の文字列を抜き出せない

「文中から正規表現で特定の文字列を抽出したい」というとき、以下のコードを書くとエラーになります。

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'

4. グローバル変数を関数内で操作しようとしてエラーになってしまう

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: 書き換え

5. readlineで改行コードつきの文字列を取得してしまう

次のようなファイル(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行目:“1” + 改行コード
  • 2行目:“12” + 改行コード
  • 3行目:“123”(最終行のため改行コードなし

1行目と2行目の末尾には1文字分の改行コードがあり、3行目はファイルの終端のため改行コードがありません。つまりreadline()は、この改行コードごと文字列を取得してしまうのです。

メソッド自体が「1行ずつファイルの文字列を取得する」という挙動のものなのだから、「まさかその行の区切りである改行コードまで取得するとは……」と驚く方も多いでしょう。筆者も最初はこれを知らず「ファイルに書かれたURLを取得してアクセスしようとしたら、末尾に改行コードが混じっていたせいで存在しないURLと見なされた」という経験があります。

改行が含まれることを想定して、あらかじめ改行コードがあれば文字を削除するよう実装するのが良いでしょう。

6. 使いまわされることを知らずに、キーワード引数のデフォルトにlistやdictを指定してしまう

引数で指定した要素を持った配列を作成する関数として、以下のようなコードを書いたとしましょう。

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の技術を身につけてください!

こちらもおすすめ!▼

SHARE

  • 広告主募集
  • ライター・編集者募集
  • WorkshipSPACE
エンジニア副業案件
Workship