Pythonでデバイスを制御しよう

第2回:プッシュボタンを扱う(5) プログラムをストップさせる例外を捕捉して処理する

「プッシュボタンを扱う」のパート5では、想定外の事象に対処する例外処理について解説します。

 

例外とはプログラムの実行中に予測していなかった事象をシステムが通知する仕組みです。

 

電子機器の制御には無限ループを使ったコーディング手法がよく利用されますが、例外はこのループを終了させてしまうため「気がついたら機器が動いていなかった」ということになりかねません。

長時間の稼働が前提となる電子機器には例外の適切な処理が必須です。そこで本記事ではPythonのプログラムを作成しながら「例外とは何か?」から「例外をどのように扱うべきか?」などを説明します。

    

目次

  1. 例外処理を検証するプログラムの構成
  2. 例外とは
  3. Pythonの例外はraiseで発行、try〜exceptで捕捉する
    1. 3.1. Pythonでの例外の発行と捕捉
    2. 3.2. 例外処理のないPythonプログラム p5-1.py
    3. 3.3. 例外を捕捉するPythonプログラム p5-2.py
    4. 3.4. 例外処理でリカバリするPythonプログラム p5-3.py
  4. Pythonではfinallyも使える
    1. 4.1. finallyブロックとは
    2. 4.2. 通常処理とfinallyを組み合わせたPythonプログラム p5-4.py
    3. 4.3. 例外処理とfinallyを組み合わせたPythonプログラム p5-5.py
    4. 4.4. finallyはどのような時に使用するのか?
  5. 例外に合わせて対応を変える
    1. 5.1. 複数の例外に対処
    2. 5.2. 複数の例外処理を実装したPythonプログラム p5-6.py
  6. まとめ

 

例外処理を検証するプログラムの構成

パート5・パート6では「例外とは何か?」「例外処理とは?」といった内容をPythonのプログラムを作成しながら解説していきます。以降、作成するプログラムの構成は以下の通り(全プログラム共通)です。

システム構成

Pythonでの例外の考え方はハードウェアに依存しません。Pythonが動作する環境であれば以降に紹介するプログラムは全て検証できるはずです。

 

例外とは

Pythonに限った話ではありませんが、コンピュータのプログラムはさまざまな状況を想定しながら「もし〜だったら〜する」のようにコーディングしていきます。

 

ところが細心の注意を払っても、ありとあらゆるケースには対応できません。中にはプログラムの実行をそのまま進められない事象もあり、そのようなケースに直面するとプログラムは異常終了します。

「プログラムの実行をそのまま進められない事象」をプログラムでは「例外」と呼び、それをシステムが通知することを「例外が発生した」「例外が発行された」「例外が生成された」などと表現します。

 

例外が発行された後、何もしなければプログラムは異常終了されます。

一方で「例外処理」とは、例外が発行されたことをプログラム内で検出して、リカバリ処理によってプログラムの実行を安全に停止することです。例外の発行を検出することを「例外を捕捉する」と言います。「捕捉」のことを「トラップ」と表現する場合もあります。

例外の発行と捕捉

今回のパート5では例外処理について、パート6では自ら例外を発行させる方法について解説します。

 

Pythonの例外はraiseで発行、try〜exceptで捕捉する

例外機能はPythonに限らず多くのプログラミング言語に取り入れられています。例外の取り扱い方法を一度マスターすれば言語が変わっても対応できるでしょう。本記事では特にほかの言語と考え方が近い部分を中心に解説を進めます。

 

Pythonでの例外の発行と捕捉

Pythonでは「raise文」で例外を発行します。また、通常の処理を実行している間(tryからexceptの間)に発生した例外を捕捉して対処するソースコードを「exceptブロック」に記述します。

Pythonの例外処理

例外を発行するraise文についてはパート6で解説します。今回のパート5では例外を捕捉するソースコードを試すためにraise文を使用します。

以降はパート3でも紹介した無限ループを抜けるためのKeyboardInterrupt例外を捕捉しない場合/する場合のPythonプログラムを紹介します。

 

例外処理のないPythonプログラム p5-1.py

以下は例外処理をしないプログラムです。

単に2秒ごと「sleep中…」と表示します。プログラムを終了するには「Ctrl+c」を押してください。

#!/usr/bin/env python

import sys
import time


def main():
    
    # 無限ループ
    while(True):
        print("sleep中...")
        time.sleep(2)
    
    print("処理を終了します")
    return 0


if __name__ == "__main__":
    sys.exit(main())

プログラムの実行中に「Ctrl+c」を押すとターミナル上は以下のような状態になるはずです。

sleep中...
sleep中...
sleep中...
^CTraceback (most recent call last):
  File "/mnt/usb0/devp/devp-py/button/./p5-1.py", line 19, in <module>
    sys.exit(main())
  File "/mnt/usb0/devp/devp-py/button/./p5-1.py", line 12, in main
    time.sleep(2)
KeyboardInterrupt

 

表示されるメッセージの意味はパート3で説明しました。

最後に「処理を終了します」のメッセージがないことから、プログラムは「Ctrl+c」を押した直後に異常終了していることが分かります。

 

例外を捕捉するPythonプログラム p5-2.py

以下は例外を捕捉して例外処理を実装したプログラムです。

#!/usr/bin/env python

import sys
import time


def main():
    
    try:
        # 無限ループ
        while(True):
            print("sleep中...")
            time.sleep(2)
    
    # 例外処理
    except KeyboardInterrupt:
        print("例外'KeyboardInterrupt'を捕捉")
    
    print("処理を終了します")
    return 0


if __name__ == "__main__":
    sys.exit(main())

以下がプログラムの実行中に「Ctrl+c」を押した際の表示です。

sleep中...
sleep中...
sleep中...
^C例外'KeyboardInterrupt'を捕捉
処理を終了します

「Ctrl+c」が押されても、いきなりプログラムが終了するわけではなく
「例外’KeyboardInterrupt’を捕捉」とメッセージを表示します。

また、最後に「処理を終了します」とメッセージがあることからプログラムを安全に終了させていることが分かります。

メッセージ中の「^C」は「Ctrl+c」が押されたことのエコーバックです。

 

例外処理でリカバリするPythonプログラム p5-3.py

前述のプログラム「p5-2.py」は例外が発生した際にプログラムを安全に停止させる例です。

以下のプログラムでは例外を捕捉した後、処理を継続(リカバリ)します。プログラムを終了するには「Ctrl+c」を2回押してください。

#!/usr/bin/env python

import sys
import time


def main():
    
    loop = 0
    
    # 無限ループ
    while(True):
        try:
            print("sleep中...")
            time.sleep(2)
        
        # 例外処理
        except KeyboardInterrupt:
            loop += 1
            print("例外'KeyboardInterrupt'を捕捉 {}回目".format(loop))
            if 2 <= loop:
                break
            print("処理を継続します")
    
    print("処理を終了します")
    return 0


if __name__ == "__main__":
    sys.exit(main())

プログラムの実行中に「Ctrl+c」を1回押しても以下のように表示され処理は継続します。

sleep中...
sleep中...
sleep中...
^C例外'KeyboardInterrupt'を捕捉 1回目
処理を継続します
sleep中...

もう一度「Ctrl+c」を押すとプログラムは安全に停止します。

tryからexceptまでのtryブロックやexceptブロックを局所的に使用している点がポイントです。

このように例外処理例外を捕捉した後、プログラムを終了させるだけでなく、エラーの原因となった事象を排除して再度実行するといったリカバリ処理にも利用できます。

 

Pythonではfinallyも使える

ほかのプログラミング言語の中には「tryブロック、exceptブロック」を使った例外処理のほかに、finallyブロックを使える言語があります。Pythonもこのfinallyブロックに対応しています。

 

finallyブロックとは

Pythonでは「raise文」で例外を発行し、通常の処理を実行している間(tryからexceptの間)に発生した例外を捕捉して対処するソースコードを「exceptブロック」に記述することは既に説明しました。

基本的に例外処理は、これらを組み合わせることでカバーできますが、Pythonにはもう一つ便利な機能「finallyブロック」があります。

 

finallyブロックは例外が発行された/されないにかかわらず、必ず実装したソースコードが実行されます。

以下に「通常処理とfinallyブロックを組み合わせた例」と「例外処理とfinallyブロックを組み合わせた例」を紹介します。

 

通常処理とfinallyを組み合わせたPythonプログラム p5-4.py

以下が通常処理とfinallyブロックを組み合わせたプログラムです。

プログラム中で
a = 1 / 1」
としていますが特に意味はありません。

 

また、今回はゼロで割り算した際に発行されるZeroDivisionError例外を捕捉するように例外処理を追加します。

#!/usr/bin/env python

import sys
import time


def main():
    
    try:
        # 通常時の処理
        a = 1 / 1
    
    # ゼロで除算した際の例外を捕捉
    except ZeroDivisionError:
        print("例外'ZeroDivisionError'を捕捉")
    
    finally:
        print("finally")
    
    print("処理を終了します")
    return 0


if __name__ == "__main__":
    sys.exit(main())

上記プログラムを実行させるとターミナル上に次のように表示されます。

finally
処理を終了します

次は例外処理とfinallyブロックを組み合わせたプログラムです。結果を比較します。

 

例外処理とfinallyを組み合わせたPythonプログラム p5-5.py

以下が例外処理とfinallyブロックを組み合わせたプログラムです。

今回はプログラム中で
a = 1 / 0」
として故意にZeroDivisionError例外を発行します。

#!/usr/bin/env python

import sys
import time


def main():
    
    try:
        # ゼロで割り算して故意に例外を発行
        a = 1 / 0
    
    # ゼロで除算した際の例外を捕捉
    except ZeroDivisionError:
        print("例外'ZeroDivisionError'を捕捉")
    
    finally:
        print("finally")
    
    print("処理を終了します")
    return 0


if __name__ == "__main__":
    sys.exit(main())

上記プログラムを実行させるとターミナル上に次のように表示されます。

例外'ZeroDivisionError'を捕捉
finally
処理を終了します

2本のプログラム「p5-4.py」「p5-5.py」を比較しても分かるように、finallyブロックは例外のある/なしにかかわらず実行されています。

 

finallyはどのような時に使用するのか?

finallyブロックを実装した処理は、例外のある/なしにかかわらず実行されます。

これは本記事でも使用するRPi.GPIOのcleanup()関数の実行に便利です。ほかにも「オープンしたファイルを適切に閉じる」「通信中のデバイスに終了コマンドを送る」といった処理に利用できます。

 

実際は、「raise文」で例外を発行し、例外を捕捉するソースコードを「exceptブロック」に記述する基本的な例外処理をマスターすれば、finallyブロックを代用できます。

さらにfinallyブロックに記述された処理は
例外のある/なしにかかわらず必ず実行しなければならない終了処理」
ということを明示できるので、ソースコードの保守性は向上するでしょう。

 

例外に合わせて対応を変える

Pythonにあらかじめ組み込まれている例外は数多くあり、exceptブロックではそれぞれの例外に対して例外処理を実装することができます。

 

複数の例外に対処

Pythonには、さまざまな例外があらかじめ用意され、プログラムの実行中に想定外の事象が発生した際に発行されます。

どのような例外があるかはPythonの公式ドキュメント、組み込み例外を参照してください。

 

今までは、その中から特定の1つの例外に対して例外処理を行う例を紹介してきました。Pythonのプログラムでは複数の例外に対応できます。

以下に複数の例外処理を実装する例を紹介します。

 

複数の例外処理を実装したPythonプログラム p5-6.py

以下が複数の例外処理を実装したプログラムです。

プログラムでは

  • プログラムの実行を中断する「Ctrl+c」を検出した際に発行されるKeyboardInterrupt
  • ゼロで割り算したことを検出した際に発行されるZeroDivisionError
  • その他の例外を検出したBaseException

に対する例外処理を実装しています。

 

通常時の処理、例外処理のいずれもダミーの処理であり特別な意味はありません。自由に改変してさまざまな状況を試してみてください。

#!/usr/bin/env python

import sys
import time


def main():
    
    try:
        time.sleep(5)
        
        # 値を代入していない変数を参照し
        # 故意に例外(UnboundLocalError)を発行
        print("割り当てられていない変数を参照 {}".format(a))
        
        # ゼロで割り算して故意に例外を発行
        a = 1 / 0
    
    # 例外処理
    except KeyboardInterrupt:
        print("例外'KeyboardInterrupt'を捕捉")
    
    # ゼロで除算した際の例外を捕捉
    except ZeroDivisionError:
        print("例外'ZeroDivisionError'を捕捉")
    
    # 上記以外の例外を捕捉
    except BaseException:
        print("例外'BaseException'を捕捉")
    
    print("処理を終了します")
    return 0


if __name__ == "__main__":
    sys.exit(main())

このプログラムをそのまま実行すると5秒後に次のようなメッセージを表示してプログラムが終了します。

例外'BaseException'を捕捉
処理を終了します

これは値の割り当てられていない変数を参照する
「print(“割り当てられていない変数を参照 {}”.format(a))」
の実行によってUnboundLocalError例外が発生した際の例外処理の結果です。

なぜUnboundLocalError例外はexceptブロックに名前がないのに捕捉されたのでしょうか?

これはPythonの例外クラスの機能を利用して階層化されているためです。
組み込まれている全ての例外の階層はPythonの公式ドキュメント、例外のクラス階層を参照してください。

例外のクラス階層(抜粋)

これを見ると、発行されたはずのUnboundLocalError例外は
「BaseException ≫ Exception ≫ NameError ≫ UnboundLocalError」
と派生していることが分かります。

 

そこで
「except BaseException:」
とすることでBaseException例外から派生している全ての例外を捕捉します。

このようにPythonにあらかじめ組み込まれている例外は相当数ありますが、その全てに例外処理を実装しなくても安全に捕捉することができます。一点、exceptブロックは上に記載されたものから順に評価されるため注意が必要です。

 

仮に
「except BaseException:」
を一番上(KeyboardInterrupt例外を捕捉する前)に記述してしまうと、全ての例外を捕捉してしまい、KeyboardInterrupt例外など、個別に対応する例外処理が実行されなくなります。

BaseException例外のようなオールマイティの例外処理は最後に実装するのが鉄則です。

 

まとめ

電子機器の制御には無限ループを使った制御がよく用いられます。例外はこのループを終了させてしまうので「気がついたら機器が動作していなかった」といったトラブルの原因になり得ます。適切な例外処理を身につけておけば、より安定した制御ができるでしょう。

今回はシステムを保護するための例外処理について説明しました。これは「受け身の対処」とも言えるでしょう。次回のパート6ではこの例外を積極的に活用する「攻めの対処」を紹介します。

 

この連載の記事

プッシュボタンを扱う(1) Pythonでプッシュボタンを扱うには

プッシュボタンを扱う(2) 誤動作の原因となるチャタリングを防止する

プッシュボタンを扱う(3) ポーリング制御でプッシュボタンに反応する

プッシュボタンを扱う(4) イベント駆動でプッシュボタンに反応する

プッシュボタンを扱う(5) プログラムをストップさせる例外を捕捉して処理する(このページの記事)

プッシュボタンを扱う(6) 「raise Exception」で積極的に例外を利用する

 

エレクトロニクスやメカトロニクスを愛するみなさんに、深く愛されるサイトを目指してDevice Plusを運営中。

https://deviceplus.jp


Arduino互換機(M5Stick-C)とセンサを使った衝突回避機能付きリモコンカーをつくろう!