7.1 関数引数のデフォルト値としての式の誤用
もうたくさん学んだね、じゃあ初心者がやりがちな標準的なミスを見ていこうか。 他人の失敗から学ぶ方が自分で失敗するよりいいよね?
Pythonでは、関数に任意の引数を持たせることができる。 これらの引数にはデフォルト値を設定できるんだ。 これってとっても便利なんだけど、可変の型に設定されたときに 困ったことになることがあるんだ。例えば、次の関数定義を見てみよう:
def foo(bar=[]): # barはオプション引数
# デフォルトでは空のリストに設定されている。
bar.append("baz") # この行が問題になるかも...
return bar
ここでよくあるミスは、オプション引数の値が、その引数に値が指定されずに 関数が呼び出されるたびにデフォルト値に設定されると考えることだ。
上記のコードでは、たとえば、foo()
関数を何度も呼び出すと (bar
引数の値を指定せずに)、常に ["baz"]
が返されると 思われるかもしれない。foo()
が呼び出されるたびに (引数 bar
を指定せずに)、bar
が []
(つまり新しい空のリスト) に設定されると想定されるためだ。
でも実際に何が起こるか見てみよう:
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz", "baz"]
result = foo()
print(result) # ["baz", "baz", "baz"]
なぜ関数が foo()
を呼び出すたびに 既存のリストに baz
を追加し続けるのか? 毎回新しいリストを作成するのではないでしょうか?
この質問の答えは、Python の「内部構造」をより深く理解することにある。 具体的には、デフォルト値は関数定義時に一度だけ初期化される。
つまり、引数 bar
はデフォルト値 (つまり空のリスト) に初期化されるのは foo()
が初めて定義されたときのみ。 しかし、その後の foo()
の呼び出し (引数 bar
を指定せずに) では、関数が最初に 定義されたときに bar
に対して作成された同じリストが 使用され続ける。
このミスを回避するためによく使われる「回避策」は次の定義です:
def foo(bar=None):
if bar is None:
bar = []
bar.append("baz")
return bar
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
7.2 クラス変数の誤用
次の例を考えてみよう:
class A(object):
x = 1
class B(A):
pass
class C(A):
pass
print(A.x, B.x, C.x)
# 1 1 1
一見問題ないように見えるね。
じゃあクラス B
のフィールド x
に値を割り当ててみよう:
B.x = 2
print(A.x, B.x, C.x)
# 1 2 1
期待通りです。
ではクラス A
の変数を変更してみましょう:
A.x = 3
print(A.x, B.x, C.x)
# 3 2 3
おかしいですね、A.x
だけを変更したはずなのに、なぜ C.x
も変わったのでしょうか?
Pythonでは、クラス変数は辞書のように扱われ、 メソッド解決順 (MRO) と呼ばれるものに従います。 つまり、上記のコードでは、属性 x
が クラス C
に見つからない場合、それは ベースクラスで見つかります (上記の例では A
のみですが、Python は複数継承をサポートしています)。
つまり、C
は A
とは独立した 自分のプロパティ x
を持っていません。 したがって、C.x
への参照は実際には A.x
への参照です。 これを理解しておかないと、問題が発生する可能性があります。 Python を学ぶときは、クラス属性とその操作に特に注意してください。
7.3 例外ブロックのパラメータの誤指定
次のコードを考えてみよう:
try:
l = ["a", "b"]
int(l[2])
except ValueError, IndexError: # 両方の例外をキャッチするためだよね?
pass
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
IndexError: list index out of range
問題は、except
式がリストのように指定された例外を受け取らないことです。そのため、上記のコードでは、例外 IndexError
は except
式でキャッチされず、代わりにパラメータ IndexError
にバインドされます。
重要!
インターネット上の例でこのようなコードに出会うことがあるかもしれません。これは Python 2.x で使用されていたものです。
複数の例外を except
式でキャッチする正しい方法は、最初のパラメータをキャッチしたいすべての例外を含むタプルとして指定することです。また、可読性を高めるために、以下のように as
キーワードを使用するのも良いでしょう:
try:
l = ["a", "b"]
int(l[2])
except (ValueError, IndexError) as e:
pass
GO TO FULL VERSION