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


15

私は「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は動的に型付けされ、コアに対してポリモーフィックであるため、このような状態を通知するには、センチネルの戻り値ではなく例外が一般的に推奨される方法です。

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

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

TIA

回答:


25

「例外を使用しないでください!」という一般的なコンセンサスは、主に他の言語に由来し、時には時代遅れのものもあります。

  • C ++では、「スタックの巻き戻し」のために例外をスローするのは非常にコストがかかります。すべてのローカル変数宣言はwithPythonのステートメントのようなものであり、その変数のオブジェクトはデストラクタを実行できます。これらのデストラクタは、例外がスローされるときに実行されますが、関数から戻るときにも実行されます。この「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の原因です)。通常、例外はより正確です。

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

Cによる解決策とPythonによる模倣は、特別な値を返すことです。CではこれはNULLポインターであり、Pythonではこれは-1です。これは、特に-1Pythonで有効なインデックスのように、チェックなしで位置が文字列インデックスとして使用される場合、驚くべき結果につながります。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の開発に役立ちます。


1
詳細な回答をありがとう。私はJavaのような「例外は例外的な条件のためだけにある」という他の言語のバックグラウンドから来ているので、これは私にとって新しいことです。EAFP、EIBTIなどのような他のPythonガイドライン(メーリングリスト、ブログ投稿など)でこのように述べているPythonコア開発者はいますか?別の開発者/ボスが尋ねます。ありがとう!
JB

カスタム例外クラスのfillInStackTraceメソッドをオーバーロードして、すぐに戻るようにすることで、スタックウォークが高価になり、これが行われるので、発生させるのを非常に安くすることができます。
ジョンコーワン

イントロの最初の箇条書きは、最初は混乱を招きました。「最近のC ++での例外処理コードはゼロコストですが、例外のスローは高価です」などのように変更できますか?(lurkersの場合:stackoverflow.com/questions/13835817/…)[編集:提案された編集]
フレネル

辞書検索操作のために使用していることを注意collections.defaultdictまたはmy_dict.get(key, default)よりコードがより明確になりますtry: my_dict[key] except: return default
スティーブ・バーンズ

11

番号!- 一般的ではありません-単一クラスのコードを除き、例外は適切なフロー制御プラクティスとは見なされません。例外が条件を通知するための合理的な、またはさらに良い方法と見なされる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 343が実装され、バックポートされて以来、すべてが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 343を読むことを強くお勧めします。

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

サーチャーの例はほぼ間違いなく後方にあり、そのような関数はジェネレーターである必要があり、最初の呼び出しで最初の一致を返し、次に置換呼び出しが次の一致を返し、一致NotFoundがなくなったときに例外を発生させます


「NO!-一般的ではありません-例外は、単一クラスのコードを除き、適切なフロー制御の実践とは見なされません」。この重要な声明の根拠はどこにありますか?この答えは、反復の場合に狭く焦点を合わせているようです。人々が例外を使用すると考えるかもしれない他の多くのケースがあります。これらの他のケースでそうすることの問題は何ですか?
ダニエルウォルトリップ

@DanielWaltripさまざまなプログラミング言語では、多くの場合、例外が使用されているコードが多すぎifます。デバッグとテスト、またはコードの動作についての推論については、例外に依存しない方が良いでしょう-例外であるべきです。実稼働コードでは、単純な戻り値ではなく、スロー/キャッチを介して返される計算を見ましたデバッグ。
スティーブバーンズ

@SteveBarnesさん、ゼロによる除算エラーキャッチの例は、プログラミングが貧弱だと思われます(例外を再発生させない一般的なキャッチを使用)。
wTyeRogers

あなたの答えは次のように要約されるようです:A)「NO!」B)例外を介した制御フローが代替手段よりもうまく機能する有効な例があります。あなたの答えは一般に有益ですが、項目Aをサポートするための引数は存在しないようです。これは太字で、一次ステートメントのように思われますが、何らかのサポートを期待します。サポートされていれば、あなたの答えに反対票を投じることはありません。ああ...
wTyeRogers
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.