ホームページ >バックエンド開発 >Python チュートリアル >Python プログラマーが犯しやすい 10 の間違い
Python について
Python は、動的セマンティクスを備えたインタープリター型のオブジェクト指向の高レベル プログラミング言語です。高度なデータ構造が組み込まれており、動的型付けと動的バインディングの利点を組み合わせているため、迅速なアプリケーション開発において非常に魅力的であり、既存のコンポーネントまたはサービスを接続するためのスクリプトまたはグルー言語として使用できます。 Python はモジュールとパッケージをサポートしているため、プログラムのモジュール化とコードの再利用が促進されます。
この記事について
Python の構文は学びやすいため、Python 開発者、特にプログラミングに慣れていない開発者は、その微妙な点を見落とし、言語の機能を過小評価する可能性があります。
それを念頭に置いて、この記事では、上級の Python 開発者でも見つけるのが難しいエラーを列挙した「トップ 10」のリストを紹介します。
よくある間違い 1: 式を関数パラメータのデフォルト値として悪用する
Python では、関数パラメータにデフォルトのオプション値を提供できます。これは言語の機能ですが、変更可能なデフォルトにより混乱が生じる可能性があります。たとえば、次の Python 関数の定義を見てください:
>>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar
よくある間違いは、オプションのパラメーターを指定せずに関数が呼び出されるたびに、オプションのパラメーターがデフォルトの指定値に設定されると考えることです。たとえば、上記のコードでは、foo() を繰り返し呼び出すと (つまり、明示的に bar パラメータを指定せずに)、常に 'baz' が返されると予想されるかもしれません。これは、foo() への各呼び出しが (bar パラメータを指定せずに) 想定しているためです。 bar は [] (つまり空のリスト) に設定されます。
しかし、これを実行すると正確に何が起こるか見てみましょう:
>>> foo() ["baz"]>>> foo() ["baz", "baz"]>>> foo() ["baz", "baz", "baz"]
ええ? foo() が呼び出されるたびに、新しいリストを作成するのではなく、デフォルト値「baz」を既存のリストに追加するのはなぜですか?
答えは、関数パラメーターのデフォルト値は関数の定義時に一度だけ評価されるということです。したがって、bar パラメータは、foo() が最初に定義されたとき、デフォルト値 (空のリスト) に初期化されますが、foo() が呼び出されたとき (つまり、bar パラメータが指定されていないとき) は引き続き使用されます。 ) bar は元々初期化されたパラメータでした。
一般的な解決策は次のとおりです:
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]
よくある間違い 2: クラス変数の誤った使用
次の例を考えてみましょう:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1
通常どおり使用してください。
1
2
3
>>> B.x = 2
>>> print A.x, B.x, C.x
1 2 1
まあ、同じでしょうもう一度試してみると。
1
2
3
>>> A.x = 3
>>> A.x, B.x, C.x を出力
3 2 3
何$%#!&? ? A.x だけを変更したのに、なぜ C.x も変更したのでしょうか?
Python では、クラス変数は内部でよく引用されるメソッド解決順序 (MRO) に従って扱われます。したがって、上記のコードでは、クラス C の x 属性が見つからないため、その基本クラスを検索します (Python は多重継承をサポートしていますが、上記の例には A しかありません)。つまり、クラス C には、A から独立した独自の x 属性がありません。したがって、C.x は実際には A.x への参照です。
よくある間違い 3: 例外に間違ったパラメータを指定する
次のコードがあるとします。
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range
ここでの問題は、Except ステートメントがこの方法で指定された例外リストを受け入れないことです。対照的に、Python 2.x では、Exception を除く構文を使用して、後で使用できるように、e によって例外オブジェクトが 2 番目のオプションの引数 (この場合は e) にバインドされます。したがって、上記の例では、IndexError 例外は、Except ステートメントによって捕捉されませんが、IndexError という名前のパラメータにバインドされると発生します。
Except ステートメントで複数の例外をキャッチする正しい方法は、最初のパラメータを、キャッチするすべての例外を含むタプルとして指定することです。また、コードの移植性を高めるには、as キーワードを使用します。これは、Python 2 と Python 3 の両方が次の構文をサポートしているためです。 Local、Enクロージング、グローバル、ビルトインの略称。 「文章の意味を知る」みたいな感じですよね?実際、Python では注意が必要な点がいくつかあります。次のコードを見てみましょう。
上記の問題は、スコープ内の変数に値を代入すると、Python が自動的にそれを現在のスコープのローカル変数として扱い、その結果、同じ名前の変数が外部スコープに隠蔽されるために発生します。
以前は正常に実行されていたコードの関数本体のどこかに代入ステートメントを追加した後に UnboundLocalError が発生すると、多くの人は驚くでしょう。 (詳しくはこちらをご覧ください)
尤其是当开发者使用 lists 时,这个问题就更加常见. 请看下面这个例子:
>>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # 没有问题... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... 但是这里有问题! ... >>> foo2()
Traceback (most recent call last):
File "
File "
UnboundLocalError: local variable 'lst' referenced before assignment
嗯?为什么 foo2 报错,而foo1没有问题呢?
原因和之前那个例子的一样,不过更加令人难以捉摸。foo1 没有对 lst 进行赋值操作,而 foo2 做了。要知道, lst += [5] 是 lst = lst + [5] 的缩写,我们试图对 lst 进行赋值操作(Python把他当成了局部变量)。此外,我们对 lst 进行的赋值操作是基于 lst 自身(这再一次被Python当成了局部变量),但此时还未定义。因此出错!
常见错误 5:当迭代时修改一个列表(List)
下面代码中的问题应该是相当明显的:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # BAD: Deleting item from a list while iterating over it ...
Traceback (most recent call last):
File "
IndexError: list index out of range
当迭代的时候,从一个 列表 (List)或者数组中删除元素,对于任何有经验的开发者来说,这是一个众所周知的错误。尽管上面的例子非常明显,但是许多高级开发者在更复杂的代码中也并非是故意而为之的。
幸运的是,Python包含大量简洁优雅的编程范例,若使用得当,能大大简化和精炼代码。这样的好处是能得到更简化和更精简的代码,能更好的避免程序中出现当迭代时修改一个列表(List)这样的bug。一个这样的范例是递推式列表(list comprehensions)。而且,递推式列表(list comprehensions)针对这个问题是特别有用的,通过更改上文中的实现,得到一段极佳的代码:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]
常见错误 6: 不明白Python在闭包中是如何绑定变量的
看下面这个例子:
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
你也许希望获得下面的输出结果:
0
2
4
6
8
但实际的结果却是:
8
8
8
8
8
惊讶吧!
这之所以会发生是由于Python中的“后期绑定”行为——闭包中用到的变量只有在函数被调用的时候才会被赋值。所以,在上面的代码中,任何时候,当返回的函数被调用时,Python会在该函数被调用时的作用域中查找 i 对应的值(这时,循环已经结束,所以 i 被赋上了最终的值——4)。
解决的方法有一点hack的味道:
>>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
0
2
4
6
8
在这里,我们利用了默认参数来生成一个匿名的函数以便实现我们想要的结果。有人说这个方法很巧妙,有人说它难以理解,还有人讨厌这种做法。但是,如果你是一个 Python 开发者,理解这种行为很重要。
常见错误 7: 创建循环依赖模块
让我们假设你有两个文件,a.py 和 b.py,他们之间相互引用,如下所示:
a.py:
import b def f(): return b.x print f()
b.py:
import a x = 1 def g(): print a.f()
首先,让我们尝试引入 a.py:
>>> import a
1
可以正常工作。这也许是你感到很奇怪。毕竟,我们确实在这里引入了一个循环依赖的模块,我们推测这样会出问题的,不是吗?
答案就是在Python中,仅仅引入一个循环依赖的模块是没有问题的。如果一个模块已经被引入了,Python并不会去再次引入它。但是,根据每个模块要访问其他模块中的函数和变量位置的不同,就很可能会遇到问题。
所以,回到我们这个例子,当我们引入 a.py 时,再引入 b.py 不会产生任何问题,因为当引入的时候,b.py 不需要 a.py 中定义任何东西。b.py 中唯一引用 a.py 中的东西是调用 a.f()。 但是那个调用是发生在g() 中的,并且 a.py 和 b.py 中都没有调用 g()。所以运行正常。
但是,如果我们尝试去引入b.py 会发生什么呢?(在这之前不引入a.py),如下所示:
>>> import b
Traceback (most recent call last):
File "
File "b.py", line 1, in
import a
File "a.py", line 6, in
print f()
File "a.py", line 4, in f
return b.x
AttributeError: 'module' object has no attribute 'x'
啊哦。 出问题了!此处的问题是,在引入b.py的过程中,Python尝试去引入 a.py,但是a.py 要调用f(),而f() 有尝试去访问 b.x。但是此时 b.x 还没有被定义呢。所以发生了 AttributeError 异常。
至少,解决这个问题很简单,只需修改b.py,使其在g()中引入 a.py:
x = 1 def g(): import a # 只有当g()被调用的时候才会引入a print a.f()
现在,当我们再引入b,没有任何问题:
>>> import b >>> b.g() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'
常见错误 8: 与Python标准库中的模块命名冲突
Python一个令人称赞的地方是它有丰富的模块可供我们“开箱即用”。但是,如果你没有有意识的注意的话,就很容易出现你写的模块和Python自带的标准库的模块之间发生命名冲突的问题(如,你也许有一个叫 email.py 的模块,但这会和标准库中的同名模块冲突)。
这可能会导致很怪的问题,例如,你引入了另一个模块,但这个模块要引入一个Python标准库中的模块,由于你定义了一个同名的模块,就会使该模块错误的引入了你的模块,而不是 stdlib 中的模块。这就会出问题了。
因此,我们必须要注意这个问题,以避免使用和Python标准库中相同的模块名。修改你包中的模块名要比通过 Python Enhancement Proposal (PEP) 给Python提建议来修改标准库的模块名容易多了。
常见错误 #9: 未能解决Python 2和Python 3之间的差异
请看下面这个 filefoo.py:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()
在Python 2中运行正常:
$ python foo.py 1
key error
1
$ python foo.py 2
value error
2
但是,现在让我们把它在Python 3中运行一下:
$ python3 foo.py 1
key error
Traceback (most recent call last):
File "foo.py", line 19, in
bad()
File "foo.py", line 17, in bad
print(e)
UnboundLocalError: local variable 'e' referenced before assignment
出什么问题了? “问题”就是,在 Python 3 中,异常的对象在 except 代码块之外是不可见的。(这样做的原因是,它将保存一个对内存中堆栈帧的引用周期,直到垃圾回收器运行并且从内存中清除掉引用。了解更多技术细节请参考这里) 。
一种解决办法是在 except 代码块的外部作用域中定义一个对异常对象的引用,以便访问。下面的例子使用了该方法,因此最后的代码可以在Python 2 和 Python 3中运行良好。
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()
在Py3k中运行:
$ python3 foo.py 1
key error
1
$ python3 foo.py 2
value error
2
正常!
(顺便提一下, 我们的 Python Hiring Guide 讨论了当我们把代码从Python 2 迁移到 Python 3时的其他一些需要知道的重要差异。)
常见错误 10: 误用__del__方法
假设你有一个名为 calledmod.py 的文件:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
并且有一个名为 another_mod.py 的文件:
import mod
mybar = mod.Bar()
你会得到一个 AttributeError 的异常。
为什么呢?因为,正如这里所说,当解释器退出的时候,模块中的全局变量都被设置成了 None。所以,在上面这个例子中,当 __del__ 被调用时,foo 已经被设置成了None。
解决方法是使用 atexit.register() 代替。用这种方式,当你的程序结束执行时(意思是正常退出),你注册的处理程序会在解释器退出之前执行。
了解了这些,我们可以将上面 mod.py 的代码修改成下面的这样:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
这种实现方式提供了一个整洁并且可信赖的方法用来在程序退出之前做一些清理工作。很显然,它是由foo.cleanup 来决定对绑定在 self.myhandle 上对象做些什么处理工作的,但是这就是你想要的。
总结
Python是一门强大的并且很灵活的语言,它有很多机制和语言规范来显著的提高你的生产力。和其他任何一门语言或软件一样,如果对它能力的了解有限,这很可能会给你带来阻碍,而不是好处。正如一句谚语所说的那样 “knowing enough to be dangerous”(译者注:意思是自以为已经了解足够了,可以做某事了,但其实不是)。
熟悉Python的一些关键的细微之处,像本文中所提到的那些(但不限于这些),可以帮助我们更好的去使用语言,从而避免一些常见的陷阱。