web-dev-qa-db-ja.com

Pythonでのフロー制御のベストプラクティスの例外はありますか?

「Learning Python」を読んでいて、次のことに遭遇しました。

ユーザー定義の例外は、エラー以外の状態を通知することもできます。たとえば、検索ルーチンをコーディングして、呼び出し元が解釈するステータスフラグを返す代わりに、一致が見つかったときに例外を発生させることができます。次の例では、try/except/else例外ハンドラーがif/else戻り値テスターの作業を行います。

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

Pythonは動的に型指定され、コアに対してポリモーフィックであるため、センチネルの戻り値ではなく例外が、このような条件を通知するための一般的に推奨される方法です。

この種のことをさまざまなフォーラムで何度も議論し、Pythonを使用してループを終了するためにStopIterationを使用することを参照していますが、公式のスタイルガイド(PEP 8フロー制御の例外への直接的な参照)または開発者からのステートメントこれはPythonのベストプラクティスであると公式に述べているものはありますか?

これ( 制御フローが深刻なアンチパターンと見なされるので例外ですか?そうであれば、なぜですか? )にも、このスタイルはPythonicであるといくつかのコメンターが述べています。これは何に基づいていますか?

TIA

17
J B

一般的なコンセンサスは「例外を使用しないでください!」ほとんどが他の言語からのものであり、時代遅れのものもある。

  • C++では、 throwing例外は非常にコストがかかります 「スタックの巻き戻し」が原因です。すべてのローカル変数宣言はPythonのwithステートメントのようであり、その変数内のオブジェクトはデストラクタを実行できます。これらのデストラクタは、例外がスローされたときだけでなく、関数から戻ったときにも実行されます。この「RAIIイディオム」は統合言語機能であり、堅牢で正しいコードを書くために非常に重要です。そのため、RAIIと安価な例外は、C++がRAIIに向けて決定したトレードオフです。

  • 初期のC++では、多くのコードが例外セーフな方法で記述されていませんでした。実際にRAIIを使用しない限り、メモリやその他のリソースがリークするのは簡単です。したがって、例外をスローすると、そのコードが正しくなくなります。 C++標準ライブラリでさえ例外を使用しているため、これはもはや合理的ではありません。例外が存在しないふりをすることはできません。ただし、CコードをC++と組み合わせる場合、例外は依然として問題です。

  • Javaでは、すべての例外にスタックトレースが関連付けられています。スタックトレースは、エラーをデバッグするときに非常に役立ちますが、例外が出力されない場合は無駄な作業になります。制御フローにのみ使用されたためです。

したがって、これらの言語では、例外は「高すぎる」ため、制御フローとして使用できません。 Pythonでは、これは問題が少なく、例外がはるかに安価です。さらに、Python言語は、例外のコストを目立たないようにするオーバーヘッドにすでに悩まされています他の制御フロー構造と比較して:たとえば、明示的なメンバーシップテストでdictエントリが存在するかどうかのチェック_if key in the_dict: ..._は、エントリ_the_dict[key]; ..._にアクセスするだけで、KeyErrorが発生するかどうかをチェックするのと同じくらい高速です。機能(ジェネレータなど)は、例外の観点から設計されています。

そのため、Pythonで例外を明確に回避する技術的な理由はありませんが、戻り値の代わりに例外を使用するかどうかという疑問が残ります。例外のある設計レベルの問題は次のとおりです。

  • それらはまったく明白ではありません。関数を簡単に調べて、どの例外がスローされるかを確認することはできないため、何をキャッチするかが常にわからない場合があります。戻り値はより明確になる傾向があります。

  • 例外は、コードを複雑にするローカル以外の制御フローです。例外をスローすると、制御フローが再開する場所がわかりません。すぐに処理できないエラーの場合、これはおそらく良い考えです。条件を呼び出し元に通知する場合、これは完全に不要です。

Pythonの文化は一般的に例外を支持して傾斜していますが、やり過ぎることは簡単です。リストにそのアイテムと等しいアイテムが含まれているかどうかをチェックするlist_contains(the_list, item)関数を想像してみてください。次のように呼び出す必要があるため、結果が例外を介して伝達される場合、それは絶対に煩わしいものです。

_try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")
_

Boolを返すと、より明確になります。

_if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")
_

関数がすでに値を返すことになっている場合、特別な条件に対して特別な値を返すと、エラーが発生しやすくなります。これは、この値を確認するのを忘れてしまうためです(おそらくCの問題の1/3の原因です)。通常、例外の方が正しいです。

良い例はpos = find_string(haystack, needle)関数で、 `haystack文字列内で最初に出現するneedle文字列を検索し、開始位置を返します。しかし、もし彼らが干し草のひもに針のひもが含まれていない場合はどうでしょうか?

Cによる解決策とPythonによる模倣は特別な値を返すことです。Cではこれはnullポインタです。Pythonこれは_-1_です。 。これは、特に_-1_がPythonで有効なインデックスであるため、位置をチェックせずに文字列インデックスとして使用すると、驚くべき結果をもたらします。Cでは、NULLポインターは少なくともセグメンテーション違反を示します。

PHPでは、異なるタイプの特別な値が返されます。整数の代わりにブール値FALSEです。結局のところ、これは言語の暗黙の変換規則のため、実際にはこれ以上良くありません(ただし、Pythonと同様に、ブール値もintとして使用できることに注意してください)。一貫性のある型を返すことは、一般に非常に混乱すると見なされます。

より堅牢なバリエーションは、文字列が見つからないときに例外をスローすることでした。これにより、通常の制御フロー中に、通常の値の代わりに特別な値を誤って使用することがなくなります。

_ try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...
_

または、常に直接使用できないが、最初にラップ解除する必要があるタイプを常に返すこともできます。ブール値が例外が発生したかどうか、または結果が使用可能かどうかを示すブール値の結果ブールタプル。次に:

_pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)
_

これにより、問題をすぐに処理する必要がありますが、非常に煩わしくなります。また、関数を簡単にチェーニングできなくなります。すべての関数呼び出しには、3行のコードが必要です。 Golangは、この迷惑行為は安全に値すると考えている言語です。

要約すると、例外は完全に問題がないわけではなく、特に「通常の」戻り値を置き換える場合は、間違いなく使いすぎてしまう可能性があります。ただし、特別な条件(必ずしもエラーだけではない)を通知するために使用される場合、例外は、クリーンで直感的で使いやすく、誤用しにくいAPIの開発に役立ちます。

29
amon

NO!-一般的ではない-例外は、単一の例外を除いて、適切なフロー制御の実践とは見なされませんコードのクラス。例外が条件を通知するための妥当な、またはさらに優れた方法と見なされる1つの場所は、ジェネレーターまたはイテレーターの操作です。これらの操作は有効な結果として可能な値を返す可能性があるため、終了を通知するメカニズムが必要です。

一度に1バイトずつストリームのバイナリファイルを読み取ることを検討してください。絶対にどんな値でも潜在的に有効な結果ですが、ファイルの終わりを通知する必要があります。したがって、選択肢があり、2つの値(バイト値と有効なフラグ)を返すたびにまたはに例外が発生します。行う。 2つのケースで、使用するコードは次のようになります。

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

あるいは:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

しかし、これは PEP 34 が実装され、バックポーティングされたため、すべて with ステートメントにまとめられています。上記は非常にPythonicになります:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

Python3ではこれは次のようになりました:

for val in open(source, 'rb').read():
     processbyte(val)

PEP 34 を読んで、背景、理論的根拠、例などを示すことを強くお勧めします。

また、ジェネレータ関数を使用して終了を通知する場合、例外を使用して処理の終了を通知することも一般的です。

あなたのサーチャーの例がほぼ確実に逆であることを付け加えたいのですが、そのような関数はジェネレータであり、最初の呼び出しで最初の一致を返し、次に置換呼び出しが次の一致を返し、ある場合にNotFound例外を発生させますいいえmore一致しません。

11
Steve Barnes