例外をいつ、どのように使用すればよいですか?


20

設定

例外をいつどのように使用するかを判断するのに苦労することがよくあります。簡単な例を考えてみましょう。AbeVigodaがまだ生きているかどうかを判断するために、「http://www.abevigoda.com/」と言うWebページをスクレイピングしているとします。これを行うには、ページをダウンロードし、「Abe Vigoda」というフレーズが表示される時間を探すだけです。安倍のステータスが含まれているため、最初の外観を返します。概念的には、次のようになります。

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

where parse_abe_status(s)は「Abe Vigoda is something」という形式の文字列を取り、「something」部分を返します。

このページを安倍のステータスのためにスクレイピングするはるかに優れた、より堅牢な方法があることを議論する前に、これは私がいる一般的な状況を強調するために使用される単純で不自然な例であることを忘れないでください。

さて、このコードはどこで問題に遭遇しますか?他のエラーの中でも、いくつかの「予期される」エラーは次のとおりです。

  • download_pageページをダウンロードできない可能性があり、をスローしIOErrorます。
  • URLが正しいページを指していないか、ページが正しくダウンロードされていないため、ヒットはありません。hits空のリストです。
  • Webページが変更されたため、ページに関する想定が間違っている可能性があります。Abe Vigodaについて4件の言及があると予想されますが、5件が見つかりました。
  • 何らかの理由で、hits[0]「Abe Vigoda is something」という形式の文字列ではない可能性があるため、正しく解析できません。

最初のケースは、実際には私にとって問題ではありませんIOError。それでは、他のケースと、それらをどのように処理するかを考えてみましょう。しかし、まず、parse_abe_status可能な限り愚かな方法で実装すると仮定しましょう。

def parse_abe_status(s):
    return s[13:]

つまり、エラーチェックを行いません。次に、オプションについて説明します。

オプション1:返品 None

呼び出し元に何かが間違っていることを返すには、次のようにしますNone

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

呼び出し側が受信した場合None、私の機能から、彼は決してエイブ・ヴィゴダの言及、およびようがなかったと仮定しなければならない何かが間違っていました。しかし、これはかなり曖昧ですよね?そして、それがhits[0]我々が思っていたものではない場合には役に立ちません。

一方、いくつかの例外を入れることができます。

オプション2:例外の使用

hitsが空の場合IndexError、を試みるとがスローされhits[0]ます。しかし、呼び出し元はIndexError、私の関数によってスローされたものを処理することを期待されるべきではありませんIndexErrorfind_all_mentions彼が知っているすべてのために、それによって投げられたかもしれません。そのため、これを処理するカスタム例外クラスを作成します。

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

では、ページが変更され、予想外の数のヒットがあった場合はどうなりますか?コードはまだ機能する可能性があるため、これは壊滅的ではありませんが、呼び出し元はさらに注意を払うか、警告を記録することができます。だから私は警告を投げます:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

最後に、私たちはそれstatusが生きているか死んでいないかを見つけるかもしれません。たぶん、何らかの奇妙な理由で、今日それが判明したcomatose。それから、私は戻りたくないFalse、それは安倍が死んだことを意味するからだ。ここで何をすべきですか?おそらく例外をスローします。しかし、どのような?カスタム例外クラスを作成する必要がありますか?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

オプション3:中間

例外を伴う2番目の方法が望ましいと思いますが、その中で例外を正しく使用しているかどうかはわかりません。より経験豊富なプログラマがこれをどのように処理するかを知りたいです。

回答:


17

Pythonの推奨事項は、例外を使用して失敗を示すことです。これは、定期的に障害が発生する場合でも当てはまります。

コードの呼び出し元の観点から見てください:

my_status = get_abe_status(my_url)

Noneを返すとどうなりますか?呼び出し側がget_abe_statusが失敗した場合を具体的に処理しない場合、my_statsがNoneのまま続行しようとします。これにより、後で診断が難しいバグが発生する可能性があります。Noneをチェックしても、このコードにはget_abe_status()が失敗した理由がわかりません。

しかし、例外を発生させたらどうなりますか?呼び出し元が具体的にケースを処理しない場合、例外は最終的にデフォルトの例外ハンドラーに到達して上方に伝播します。それはあなたが望むものではないかもしれませんが、プログラムの他の場所に微妙なバグを導入するよりも優れています。さらに、この例外は、最初のバージョンで失われた問題点に関する情報を提供します。

呼び出し側から見ると、戻り値よりも例外を取得する方が簡単です。そして、それはPythonスタイルであり、例外を使用して、失敗状態が値を返さないことを示します。

別の見方をし、本当に起こるとは決して思わない場合にのみ例外を使用すべきだと主張する人もいます。彼らは、通常実行中の実行は例外を発生させるべきではないと主張します。この理由の1つは、例外が非常に非効率的であることですが、実際にはPythonには当てはまりません。

コードのいくつかのポイント:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

それは空のリストをチェックするための本当に混乱した方法です。何かをチェックするためだけに例外を誘発しないでください。ifを使用します。

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

logger.warning行が正しく実行されないことを理解していますか?


1
ご回答ありがとうございます。公開されたコードを見ることで、例外をいつどのようにスローするかについての私の気持ちが向上しました。
jme 14年

4

受け入れられた答えは受け入れられるに値し、質問に答えます。私はこれを少しだけ余分な背景を提供するために書いています。

Pythonの信条の1つは、許可よりも許しを求める方が簡単だということです。これは、通常、あなたはただ物事を行うだけで、例外が予想される場合は、それらを処理することを意味します。ifを事前にチェックして、例外が発生しないことを確認するのとは対照的です。

C ++ / Javaとのメンタリティの違いがどれほど劇的かを示す例を提供したいと思います。C ++のforループは通常、次のようになります。

for(int i = 0; i != myvector.size(); ++i) ...

これについて考える方法:myvector[k]k> = myvector.size()にアクセスすると例外が発生します。したがって、原則として、これをtry-catchとして(非常に厄介に)書くことができます。

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

または似たようなもの。次に、Python forループで何が起こっているかを考えてみましょう。

for i in range(1):
    ...

これはどのように機能しますか?forループはrange(1)の結果を受け取り、そのループに対してiter()を呼び出し、イテレーターを取得します。

b = range(1).__iter__()

次に、ループの各反復でnextを呼び出します。...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

言い換えれば、Pythonのforループは実際には偽装のtry-exceptです。

具体的な質問に関する限り、例外は通常の関数の実行を停止するため、個別に処理する必要があることに注意してください。Pythonでは、関数内の残りのコードを実行するポイントがない場合、および/または関数内で発生した内容を正確に反映する戻り値がない場合は、それらを自由にスローする必要があります。関数から早期に戻ることは異なることに注意してください。早期に戻すということは、すでに答えを把握しており、答えを把握するために残りのコードを必要としないことを意味します。答えがわからない場合は例外をスローする必要があり、答えを決定する残りのコードは合理的に実行できないと言っています。さて、あなたがスローすることを選択した例外のように、それ自身を「正しく反映する」ことはすべて文書の問題です。

特定のコードの場合、ヒットが空のリストになるような状況はスローする必要があります。どうして?さて、関数の設定方法では、ヒットを解析せずに答えを決定する方法はありません。そのため、URLが不正であるか、ヒットが空であるためにヒットが解析できない場合、関数は質問に答えることができず、実際に実際に試みることさえできません。

この特定のケースでは、解析できて合理的な答え(生きているか死んでいる)を受け取らなくても、まだ投げるべきだと主張します。どうして?なぜなら、関数はブール値を返すからです。Noneを返すことは、クライアントにとって非常に危険です。Noneでifチェックを行った場合、失敗することはなく、静かにFalseとして扱われます。したがって、クライアントは基本的に、サイレントフェールが必要ない場合は、if is Noneチェックを常に行う必要があります。


2

例外が発生した場合は、例外を使用する必要があります。つまり、アプリケーションが適切に使用されていれば、発生してはならないことです。メソッドのコンシューマが見つからないものを検索することが許容され、予想される場合、「見つかりません」は例外的なケースではありません。この場合、nullまたは "None"または{}、または空のリターンセットを示す何かを返す必要があります。

一方、メソッドのコンシューマーが常に(何らかの方法で混乱していない限り)検索対象を見つけることを期待している場合、それが見つからない場合は例外となり、それを使用する必要があります。

重要なのは、例外処理が高価になる可能性があることです。例外は、スタックトレースなど、アプリケーションの状態に関する情報を収集して、人々が発生した理由を理解できるようにするためのものです。それはあなたがやろうとしていることだとは思わない。


1
値を見つけなくてもよいと判断した場合は、何が起こったかを示すために何を使用するかに注意してください。メソッドがaを返すことになっていStringて、インジケータとして「なし」を選択した場合、これは「なし」が有効な値にならないように注意する必要があることを意味します。また、データを見て値を見つけないこととデータを取得できないことには違いがあることに注意してください。したがって、データを見つけることができません。これらの2つのケースで同じ結果が得られるということは、1つの値が期待されるときに値が得られない場合、可視性がないことを意味します。
-unholysampler

インラインコードブロックにはバッククォート( `)が付けられていますが、それはおそらく「なし」で何をするつもりなのでしょうか?
イズカタ

3
Pythonでこれは絶対に間違っていると思います。C ++ / Javaスタイルの推論を別の言語に適用しています。Pythonは、forループの終わりを示すために例外を使用します。それはかなり例外的ではありません。
ニルフリードマン14

2

関数を書いていた場合

 def abe_is_alive():

私はreturn TrueどちらかFalseに絶対に確信している場合、またはraise他の場合にはエラー(例えばraise ValueError("Status neither 'dead' nor 'alive'"))にそれを書きます。これは、私の呼び出し元の関数がブール値を予期しているためであり、それを確実に提供できない場合、通常のプログラムフローは続行されません。

予想とは異なる「ヒット」数を得るあなたの例のようなものは、おそらく無視するでしょう。ヒットの1つが私のパターン「Abe Vigoda is {dead | alive}」に一致する限り、それは問題ありません。これにより、ページを再配置できますが、適切な情報を取得できます。

のではなく

try:
    hits[0] 
except IndexError:
    raise NotFoundError

私は明示的にチェックします:

if not hits:
    raise NotFoundError

これは「安い」傾向があるため、を設定しますtry

私はあなたに同意しますIOError; また、ウェブサイトへの接続をエラー処理しようとはしません-何らかの理由でこれを処理するのに適切な場所ではない場合(質問への回答に役立たないため)、合格するはずです呼び出し関数に。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.