バギーPythonコード:Python開発者が犯す最も一般的な10の間違い

公開: 2022-03-11

Pythonについて

Pythonは、動的セマンティクスを備えた、解釈されたオブジェクト指向の高級プログラミング言語です。 動的型付けと動的バインディングを組み合わせた高レベルの組み込みデータ構造により、Rapid Application Developmentだけでなく、既存のコンポーネントやサービスを接続するためのスクリプト言語またはグルー言語としても非常に魅力的です。 Pythonはモジュールとパッケージをサポートしているため、プログラムのモジュール性とコードの再利用が促進されます。

この記事について

Pythonのシンプルで習得しやすい構文は、Python開発者、特にその言語に不慣れな開発者を誤解させて、その微妙な点のいくつかを見逃し、多様なPython言語の力を過小評価する可能性があります。

そのことを念頭に置いて、この記事では、後部のより高度なPython開発者でさえも噛み付く可能性のある、やや微妙で見つけにくい間違いの「トップ10」リストを紹介します。

(注:この記事は、Pythonプログラマーのよくある間違いよりも上級者を対象としています。Pythonプログラマーは、この言語に慣れていない人を対象としています。)

よくある間違い#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"を追加し続けるのはなぜですか?

より高度なPythonプログラミングの答えは、関数の引数のデフォルト値は、関数が定義されたときに一度だけ評価されるというものです。 したがって、 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 Ax, Bx, Cx 1 1 1

理にかなっています。

 >>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1

うん、やはり予想通り。

 >>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3

$%#!& ?? Axのみを変更しました。 なぜCxも変わったのですか?

Pythonでは、クラス変数は内部でディクショナリとして処理され、メソッド解決順序(MRO)と呼ばれることが多いものに従います。 したがって、上記のコードでは、属性xがクラスCに見つからないため、その基本クラスで検索されます(Pythonは複数の継承をサポートしていますが、上記の例ではAのみ)。 つまり、 Cには、 Aとは独立した独自のxプロパティがありません。 したがって、 Cxへの参照は実際にはAxへの参照です。 これは、適切に処理されない限り、Pythonの問題を引き起こします。 Pythonのクラス属性の詳細をご覧ください。

よくある間違い#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では、 except Exception, e構文を使用して、指定されたオプションの2番目のパラメーター(この場合はe )に例外をバインドし、さらに検査できるようにします。 その結果、上記のコードでは、 IndexError例外はexceptステートメントによってキャッチされていません。 むしろ、例外は代わりにIndexErrorという名前のパラメーターにバインドされることになります。

exceptステートメントで複数の例外をキャッチする適切な方法は、キャッチするすべての例外を含むタプルとして最初のパラメーターを指定することです。 また、移植性を最大化するには、 asキーワードを使用します。これは、その構文がPython2とPython3の両方でサポートされているためです。

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>

よくある間違い#4:Pythonスコープルールの誤解

Pythonスコープの解決は、LEGBルールと呼ばれるものに基づいています。これは、 L ocal、 E nclosingG lobal、Built-inの省略形です。 簡単そうですね。 実際、これがPythonで機能する方法には微妙な点がいくつかあります。これにより、以下の一般的なより高度なPythonプログラミングの問題が発生します。 次のことを考慮してください。

 >>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment

どうしたの?

上記のエラーが発生するのは、スコープ内の変数に割り当てを行うと、その変数がPythonによって自動的にそのスコープに対してローカルであると見なされ、外部スコープ内の同様の名前の変数がシャドウされるためです。

これにより、関数の本体のどこかに代入ステートメントを追加して変更したときに、以前に機能していたコードでUnboundLocalErrorが発生することに多くの人が驚いています。 (これについて詳しくは、こちらをご覧ください。)

リストを使用するときに、これが開発者をつまずかせるのは特に一般的です。 次の例を考えてみましょう。

 >>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment

は? foo1が正常に実行されているのに、なぜfoo2が爆弾を投下したのですか?

答えは前の例の問題と同じですが、確かにより微妙です。 foo1foo2割り当てを行っていませんが、 lstは割り当てを行っています。 lst += [5]は実際にはlst = lst + [5]の省略形であることを思い出すと、 lstに値を割り当てようとしていることがわかります(したがって、Pythonによってローカルスコープにあると推定されます)。 ただし、 lstに割り当てようとしている値は、まだ定義されていないlst自体に基づいています(ここでも、ローカルスコープにあると推定されます)。 ブーム。

よくある間違い#5:リストを繰り返しながら変更する

次のコードの問題はかなり明白なはずです。

 >>> 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 "<stdin>", line 2, in <module> IndexError: list index out of range

繰り返しながらリストまたは配列からアイテムを削除することは、経験豊富なソフトウェア開発者にはよく知られているPythonの問題です。 上記の例はかなり明白かもしれませんが、高度な開発者でさえ、はるかに複雑なコードでこれに意図せずに噛まれる可能性があります。

幸い、Pythonには多くの洗練されたプログラミングパラダイムが組み込まれており、適切に使用すると、コードが大幅に簡素化および合理化されます。 これの副次的な利点は、単純なコードが、リスト項目の偶発的な削除中に繰り返し処理されるバグに噛まれる可能性が低いことです。 そのようなパラダイムの1つは、リスト内包のパラダイムです。 さらに、リスト内包表記は、完全に機能する上記のコードのこの代替実装によって示されるように、この特定の問題を回避するために特に役立ちます。

 >>> 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の遅延バインディング動作が原因で発生します。 したがって、上記のコードでは、返された関数のいずれかが呼び出されるたびに、 iの値は、呼び出されたときに周囲のスコープで検索されます(そして、それまでにループが完了しているため、 iにはすでにそのfinalが割り当てられています4の値)。

この一般的なPythonの問題の解決策は、ちょっとしたハックです。

 >>> 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.pyb.pyの2つのファイルがあり、それぞれが他方をインポートするとします。

a.py

 import b def f(): return bx print f()

そしてb.pyで:

 import a x = 1 def g(): print af()

まず、 a.pyをインポートしてみましょう:

 >>> import a 1

うまくいきました。 おそらくそれはあなたを驚かせます。 結局のところ、ここには循環インポートがありますが、これはおそらく問題になるはずですよね?

答えは、循環インポートの単なる存在は、それ自体がPythonの問題ではないということです。 モジュールがすでにインポートされている場合、Pythonはそれを再インポートしようとしないほど賢いです。 ただし、各モジュールが他のモジュールで定義された関数または変数にアクセスしようとしているポイントによっては、実際に問題が発生する可能性があります。

したがって、例に戻ると、 a.pyをインポートすると、 b.pyのインポートに問題はありませんでした。これは、 b.pyは、インポートa.pyから何も定義する必要がないためです。 b.pyaへの唯一の参照は、 af()の呼び出しです。 しかし、その呼び出しはg()にあり、 a.pyまたはb.pyには何もg( g()を呼び出しません。 だから人生は良いです。

しかし、(以前にb.pyをインポートせずに) a.pyをインポートしようとするとどうなりますか?

 >>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'

ええとああ。 それは良いことではありません! ここでの問題は、 a.pyをインポートするプロセスで、 b.pyをインポートしようとし、次にf()を呼び出してbxにアクセスしようとすることです。 しかし、 bxはまだ定義されていません。 したがって、 AttributeError例外。

これに対する少なくとも1つの解決策は非常に簡単です。 a.pyを変更して、 g()b.pyをインポートするだけです。

 x = 1 def g(): import a # This will be evaluated only when g() is called print af()

いいえ、インポートすると、すべて問題ありません。

 >>> import b >>> bg() 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の美しさの1つは、「すぐに使える」ライブラリモジュールが豊富に用意されていることです。 しかし、その結果、意識的に回避しなければ、モジュールの1つの名前と、Pythonに付属している標準ライブラリ内の同じ名前のモジュール(たとえば、 、コードにemail.pyという名前のモジュールが含まれている可能性があります。これは、同じ名前の標準ライブラリモジュールと競合します)。

これは、モジュールのPython標準ライブラリバージョンをインポートしようとする別のライブラリをインポートするなどの厄介な問題につながる可能性がありますが、同じ名前のモジュールがあるため、他のパッケージが、内のモジュールではなく、誤ってバージョンをインポートしますPython標準ライブラリ。 これは悪いPythonエラーが発生する場所です。

したがって、Python標準ライブラリモジュールと同じ名前を使用しないように注意する必要があります。 パッケージ内のモジュールの名前を変更する方が、Python拡張プロポーザル(PEP)を提出してアップストリームで名前の変更を要求し、それを承認するよりもはるかに簡単です。

よくある間違い#9:Python2とPython3の違いに対処できない

次のファイルfoo.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

しかし、Python3でそれを回転させましょう。

 $ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment

ここで何が起こったのですか? 「問題」は、Python 3では、 exceptブロックのスコープを超えて例外オブジェクトにアクセスできないことです。 (この理由は、そうでない場合、ガベージコレクタが実行されてメモリから参照が削除されるまで、メモリ内のスタックフレームで参照サイクルが維持されるためです。これに関する技術的な詳細については、こちらを参照してください)。

この問題を回避する1つの方法は、 exceptブロックのスコープにある例外オブジェクトへの参照を維持して、アクセス可能な状態を維持することです。 これは、この手法を使用する前の例のバージョンであり、Python2とPython3の両方に対応したコードを生成します。

 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採用ガイドでは、コードをPython2からPython3に移行するときに注意すべき他の重要な違いについて説明しています。)

よくある間違い#10: __del__メソッドの誤用

mod.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に設定されています。

このやや高度なPythonプログラミングの問題の解決策は、代わりに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)

この実装は、通常のプログラム終了時に必要なクリーンアップ機能を呼び出すためのクリーンで信頼性の高い方法を提供します。 明らかに、 self.myhandleという名前にバインドされたオブジェクトをどう処理するかを決定するのはfoo.cleanup次第ですが、あなたはその考えを理解します。

要約

Pythonは、生産性を大幅に向上させることができる多くのメカニズムとパラダイムを備えた強力で柔軟な言語です。 ただし、他のソフトウェアツールや言語と同様に、その機能の理解や認識が限られていると、メリットよりも障害になることがあり、「危険なことを十分に知っている」ということわざの状態になります。

この記事で提起された中程度に高度なプログラミングの問題など、Pythonの重要なニュアンスに精通することは、言語の使用を最適化すると同時に、より一般的なエラーのいくつかを回避するのに役立ちます。

また、Pythonの専門家を特定するのに役立つ面接の質問に関する提案については、Python面接のインサイダーガイドを確認することをお勧めします。

この記事のポインタがお役に立てば幸いです。フィードバックをお待ちしております。