エラーハンドリングと例外

Beginner

This tutorial is from open-source community. Access the source code

はじめに

例外は以前から導入されていますが、このセクションでは、エラーチェックと例外処理に関する追加の詳細を記載します。

これは Guided Lab です。学習と実践を支援するためのステップバイステップの指示を提供します。各ステップを完了し、実践的な経験を積むために、指示に注意深く従ってください。過去のデータによると、この 初級 レベルの実験の完了率は 85%です。学習者から 100% の好評価を得ています。

プログラムが失敗する方法

Python は、関数の引数の型や値のチェックや検証を行いません。関数は、関数内の文に対応する任意のデータで機能します。

def add(x, y):
    return x + y

add(3, 4)               ## 7
add('Hello', 'World')   ## 'HelloWorld'
add('3', '4')           ## '34'

関数にエラーがある場合、それらは実行時に例外として表示されます。

def add(x, y):
    return x + y

>>> add(3, '4')
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +:
'int' and 'str'
>>>

コードを検証するには、テスト(後述)に重点が置かれます。

例外

例外はエラーを通知するために使用されます。自分で例外を発生させるには、raise 文を使用します。

if name not in authorized:
    raise RuntimeError(f'{name} not authorized')

例外をキャッチするには、try-except を使用します。

try:
    authenticate(username)
except RuntimeError as e:
    print(e)

例外処理

例外は最初の一致する except に伝播します。

def grok():
  ...
    raise RuntimeError('Whoa!')   ## ここで例外が発生します

def spam():
    grok()                        ## 例外を発生させる呼び出し

def bar():
    try:
       spam()
    except RuntimeError as e:     ## ここで例外がキャッチされます
      ...

def foo():
    try:
         bar()
    except RuntimeError as e:     ## 例外はここには到達しません
      ...

foo()

例外を処理するには、except ブロックに文を記述します。エラーを処理するために必要な任意の文を追加できます。

def grok():...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   ## ここで例外がキャッチされます
        statements              ## この文を使用します
        statements
      ...

bar()

処理後、実行は try-except の直後の最初の文から再開されます。

def grok():...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   ## ここで例外がキャッチされます
        statements
        statements
      ...
    statements                  ## ここで実行が再開されます
    statements                  ## そしてここから続行されます
  ...

bar()

組み込み例外

組み込みの例外は約 20 種類あります。通常、例外の名前は何が間違っているかを示しています(たとえば、不適切な値を提供したために ValueError が発生します)。これは網羅的な一覧ではありません。詳細については、ドキュメント を参照してください。

ArithmeticError
AssertionError
EnvironmentError
EOFError
ImportError
IndexError
KeyboardInterrupt
KeyError
MemoryError
NameError
ReferenceError
RuntimeError
SyntaxError
SystemError
TypeError
ValueError

例外値

例外には関連付けられた値があります。それは何が間違っているかに関するより具体的な情報を含んでいます。

raise RuntimeError('Invalid user name')

この値は、except に渡される変数に格納される例外インスタンスの一部です。

try:
 ...
except RuntimeError as e:   ## `e` には発生した例外が格納されます
 ...

e は例外型のインスタンスです。ただし、印刷すると文字列のように見えることがよくあります。

except RuntimeError as e:
    print('Failed : Reason', e)

複数のエラーのキャッチ

複数の except ブロックを使用して、さまざまな種類の例外をキャッチすることができます。

try:
...
except LookupError as e:
...
except RuntimeError as e:
...
except IOError as e:
...
except KeyboardInterrupt as e:
...

あるいは、それらを処理する文が同じ場合、それらをグループ化することができます。

try:
...
except (IOError,LookupError,RuntimeError) as e:
...

すべてのエラーをキャッチする

任意の例外をキャッチするには、次のように Exception を使用します。

try:
...
except Exception:       ## 注意。以下を参照のこと
    print('An error occurred')

一般的に、そのようなコードを書くのは良い考えではありません。なぜなら、エラーが発生した理由が分からなくなってしまうからです。

エラーをキャッチする誤った方法

ここに例外を使用する誤った方法を示します。

try:
    go_do_something()
except Exception:
    print('Computer says no')

これはすべての可能性のあるエラーをキャッチしますが、コードがまったく予想していない理由(たとえば、インストールされていない Python モジュールなど)で失敗している場合、デバッグが不可能になる可能性があります。

やや良いアプローチ

すべてのエラーをキャッチする場合、これはもっと健全なアプローチです。

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)

これは、失敗の具体的な理由を報告します。すべての可能性のある例外をキャッチするコードを書くときに、エラーを表示/報告するための何らかのメカニズムを持つことは、ほとんどの場合良い考えです。

ただし、一般的には、できる限り狭い範囲でエラーをキャッチする方が良いです。実際に処理できるエラーだけをキャッチします。他のエラーは通過させましょう。たぶん他のコードがそれらを処理できるかもしれません。

例外の再送出

キャッチしたエラーを伝播させるには、raise を使用します。

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)
    raise

これにより、何らかのアクション(たとえば、ログ記録)を実行して、エラーを呼び出し元に渡すことができます。

例外処理のベストプラクティス

例外をキャッチしないでください。早期にエラーを検出して大きな音で鳴らしましょう。重要な場合は、他の人が問題を解決します。あなたがその人である場合にのみ例外をキャッチしてください。つまり、回復でき、健全に処理を続けられるエラーのみをキャッチします。

finally

これは、例外が発生してもしなくても必ず実行されるコードを指定します。

lock = Lock()
...
lock.acquire()
try:
  ...
finally:
    lock.release()  ## これは常に実行されます。例外の有無に関係なく。

一般的に、リソース(特にロック、ファイルなど)を安全に管理するために使用されます。

with

現代のコードでは、try - finally はしばしば with 文に置き換えられます。

lock = Lock()
with lock:
    ## ロックが獲得されました
  ...
## ロックが解放されました

もっと身近な例:

with open(filename) as f:
    ## ファイルを使用する
  ...
## ファイルが閉じられました

with はリソースの使用 コンテキスト を定義します。実行がそのコンテキストを抜けると、リソースが解放されます。with は、それをサポートするように特別にプログラムされた特定のオブジェクトのみと機能します。

演習 3.8:例外の送出

前節で書いた parse_csv() 関数は、ユーザー指定の列を選択できるようになっていますが、入力データファイルに列ヘッダーがある場合にのみ機能します。

コードを修正して、selecthas_headers=False の両方の引数が渡された場合に例外が送出されるようにしましょう。たとえば:

>>> parse_csv('/home/labex/project/prices.csv', select=['name','price'], has_headers=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 9, in parse_csv
    raise RuntimeError("select argument requires column headers")
RuntimeError: select argument requires column headers
>>>

この 1 つのチェックを追加したら、関数内で他の種類の妥当性チェックを行うべきかどうか尋ねるかもしれません。たとえば、ファイル名が文字列であること、types がリストであること、またはそのような性質のことをチェックするべきでしょうか?

一般的なルールとして、通常はそのようなテストを省略し、悪い入力でプログラムが失敗するようにするのがベストです。トレースバックメッセージが問題の原因を指摘し、デバッグに役立ちます。

上記のチェックを追加する主な理由は、無意味なモードでコードを実行しないようにすること(たとえば、列ヘッダーが必要な機能を使用する一方で、ヘッダーがないことを同時に指定すること)です。

これは、呼び出し元のコードのプログラミングエラーを示しています。「起こってはならない」ケースをチェックすることは、多くの場合、良い考えです。

演習 3.9:例外のキャッチ

あなたが書いた parse_csv() 関数は、ファイルの全体の内容を処理するために使用されます。しかし、現実の世界では、入力ファイルに破損、欠落、または汚染されたデータが含まれている可能性があります。この実験を試してみてください:

>>> portfolio = parse_csv('missing.csv', types=[str, int, float])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 36, in parse_csv
    row = [func(val) for func, val in zip(types, row)]
ValueError: invalid literal for int() with base 10: ''
>>>

parse_csv() 関数を修正して、レコード作成中に発生するすべての ValueError 例外をキャッチし、変換できない行に対して警告メッセージを表示するようにします。

メッセージには、行番号と失敗した理由に関する情報が含まれている必要があります。関数をテストするには、上記の missing.csv ファイルを読み取ってみてください。たとえば:

>>> portfolio = parse_csv('missing.csv', types=[str, int, float])
Row 4: Couldn't convert ['MSFT', '', '51.23']
Row 4: Reason invalid literal for int() with base 10: ''
Row 7: Couldn't convert ['IBM', '', '70.44']
Row 7: Reason invalid literal for int() with base 10: ''
>>>
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}]
>>>

演習 3.10:エラーの抑制

parse_csv() 関数を修正して、ユーザーが明示的に希望する場合には、解析エラーメッセージを抑制できるようにします。たとえば:

>>> portfolio = parse_csv('missing.csv', types=[str,int,float], silence_errors=True)
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}]
>>>

エラーハンドリングは、ほとんどのプログラムで正しく行うのが最も困難なことの 1 つです。一般的なルールとして、エラーを黙って無視してはなりません。代わりに、問題を報告し、ユーザーがそう選択した場合にエラーメッセージを抑制するオプションを与える方が良いです。

まとめ

おめでとうございます!あなたはエラーチェックの実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに多くの実験を行って練習してください。