web-dev-qa-db-ja.com

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

設定

例外をいつどのように使用するかを決定するのに苦労することがよくあります。簡単な例を考えてみましょう。 " http://www.abevigoda.com/ "のようにWebページをこすって、Abe Vigodaがまだ生きているかどうかを確認します。これを行うには、ページをダウンロードして、「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"
_

ここで、parse_abe_status(s)は、「Abe Vigoda issomething」という形式の文字列を取り、「何か」の部分。

このページを安倍氏のステータスとしてスクレイピングするより優れたより堅牢な方法があると主張する前に、これは私が現在いる一般的な状況を強調するために使用されている単純で不自然な例にすぎないことを覚えておいてください。

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

  • _download_page_はページをダウンロードできない可能性があり、IOErrorをスローします。
  • URLが正しいページをポイントしていないか、ページが正しくダウンロードされていないため、ヒットがありません。 hitsは空のリストです。
  • Webページが変更されたため、ページに関する想定が誤っていた可能性があります。多分私達はAbe Vigodaの4つの言及を期待するが、今私達は5つを見つける。
  • 何らかの理由で、_hits[0]_は "Abe Vigoda issomething"という形式の文字列ではない可能性があるため、正しく解析できません。

最初のケースは実際には問題ではありません。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を受け取った場合、彼はAbe Vigodaについての言及がないと想定する必要があるため、somethingが間違っていました。しかし、これはかなり曖昧ですよね?そして、それは、_hits[0]_が、私たちが思っていたものと異なる場合には役立ちません。

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

オプション2:例外を使用する

hitsが空の場合、_hits[0]_を試行するとIndexErrorがスローされます。しかし、呼び出し元は、IndexErrorがどこから来たかわからないため、私の関数によってスローされたIndexErrorを処理することを期待されるべきではありません。彼が知っている限りでは、それは_find_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"
_

次に、ページが変更され、予期しない数のヒットがあった場合はどうなりますか?コードは引き続き機能するため、これは致命的ではありませんが、呼び出し元はextraに注意したり、警告をログに記録したりする場合があります。だから私は警告を投げます:

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

21
jme

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

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

my_status = get_abe_status(my_url)

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

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

呼び出し側から見ると、戻り値よりも例外を取得する方が便利です。そして、それはpythonスタイルです。例外を使用して、戻り値ではなく失敗条件を示します。

一部の人は別の見方をして、本当に起こることを決して期待しない場合にのみ例外を使用すべきだと主張します。彼らは、通常実行中の実行は例外を発生させるべきではないと主張しています。この理由として挙げられるのは、例外は非常に非効率的ですが、実際には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行が正しく実行されないことを知っていますか?

17
Winston Ewert

受け入れられた答えは受け入れられるべきであり、質問に答えるに値します。これは、少し余分な背景を提供するためにのみ書きます。

Pythonの信条の1つは、許可より許しを求める方が簡単です。つまり、通常は単に処理を実行し、例外が予想される場合は処理します。 ifを事前にチェックするのではなく、例外が発生しないことを確認します。

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

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

これについて考える方法:アクセスmyvector[k]ここでk> = myvector.size()は例外を引き起こします。したがって、原則として、これを(非常にぎこちなく)トライ/キャッチとして書くことができます。

    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(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チェックを実行する必要があります。彼がサイレントな失敗を望まない場合は...なので、おそらくスローする必要があります。

4
Nir Friedman

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

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

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

2
Matthew Flynn

関数を書いていたら

_ def abe_is_alive():
_

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

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

のではなく

_try:
    hits[0] 
except IndexError:
    raise NotFoundError
_

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

_if not hits:
    raise NotFoundError
_

これは「安上がり」になる傾向があるため、tryを設定します。

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

2
jonrsharpe