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

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

プッシュボタンを扱う(1) Pythonでプッシュボタンを扱うには
プッシュボタンを扱う(2) 誤動作の原因となるチャタリングを防止する
プッシュボタンを扱う(3) ポーリング制御でプッシュボタンに反応する
プッシュボタンを扱う(4) イベント駆動でプッシュボタンに反応する
プッシュボタンを扱う(5) プログラムをストップさせる例外を捕捉して処理する

 

「プッシュボタンを扱う」のパート6では、積極的に例外を発行する方法について解説します。

前回のパート5ではプログラムの実行を止めてしまう厄介な例外と、その処理方法について説明しました。言ってみれば「守りの例外処理」です。今回はその例外をプログラム内で自ら発行して安全で効率の良いシステムを構築する「攻めの例外処理」を紹介します。

ちなみに、この「例外の発行する」という言葉は「例外を生成する」「例外を投げる(スローする)」とも表現されます。いずれも同じ意味です。本記事でも「例外を発行する/生成する/投げる/スローする」と表現します。

 

目次

  1. Pythonのプログラムで積極的に例外を投げるには
    1. 1.1. 例外を投げるには「raise Exception」を使う
    2. 1.2. 例外を投げるPythonプログラム p6-1.py
    3. 1.3. プログラムの説明
  2. どういった際に例外を投げるのか?
    1. 2.1. 例外を投げて効率を良くできるケース
    2. 2.2. 例外を投げないPythonプログラム p6-2.py
    3. 2.3. 例外を投げるPythonプログラム p6-3.py
    4. 2.4. プログラムの説明
  3. PythonのExceptionを拡張して自作の例外を投げる
    1. 3.1. なぜ例外を自作するのか?
    2. 3.2. 自作した例外を投げるPythonプログラム p6-4.py
    3. 3.3. 例外を自作する
    4. 3.4. 例外を投げる/捕捉する
    5. 3.5. 複数の例外を自作し処理を切り分けるPythonプログラム p6-5.py
    6. 3.6. 自作した例外で処理が切り分けられることを確認
  4. まとめ

 

Pythonのプログラムで積極的に例外を投げるには

例外はシステムに想定外の出来事が起こった時に発行されますが、これができるのはシステム側だけではありません。我々プログラムを作る側も自由に例外を投げることができます。

例外を投げるには「raise Exception」を使う

自ら例外を投げる利点については後ほど解説しますが、まずはPythonのプログラムで例外を投げる方法を覚えましょう。

Pythonのプログラムで例外を投げる際には「raise文」を使用します。

例外を発行したい場所で
「raise 例外の名前(メッセージ)」
と記述します。

 

「例外の名前」はあらかじめ登録されている例外クラスを指定。例外クラスは独自に作成することもでき、方法は後述します。パート5でも解説したように例外の捕捉はこの名前によって行われます。

「メッセージ」には文字列を指定。文字列は自由に決めることができ、例外の原因となった情報を設定するのが一般的です。省略可能ですが、ほかの例外と互換させるためにも指定するのをお勧めします。

 

例外を投げるPythonプログラム p6-1.py

以下は自ら例外を投げ、捕捉するPythonのプログラムです。

#!/usr/bin/env python

import sys


def main():
    
    try:
        print("主処理がスタートしました")
        
        # 例外の発行
        raise Exception("想定外の事象が発生しました!")
        
        # この行は例外の発行でスキップされる
        print("主処理が終了します")
    
    except Exception as e:
        print("例外'Exception'を捕捉: " + e.args[0])
    
    return 0


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

 

プログラムの説明

上記のプログラム「p6-1.py」を実行するとターミナル上には次のように表示されるはずです。

 

主処理がスタートしました
例外’Exception’を捕捉: 想定外の事象が発生しました!

 

ターミナルに表示された1行目の「主処理がスタートしました」はtryブロックの先頭の行が出力したものです。

続く「例外’Exception’を捕捉: 想定外の事象が発生しました!」はexceptブロック内の行が出力しました。

tryブロックの3行目「print(“主処理が終了します”)」は処理されません。

これは2行目の
「raise Exception(“想定外の事象が発生しました!”)」
の実行によって例外が発行され、以降の処理をスキップ、exceptブロックに捕捉されたためです。

raiseで例外が発行される

 

自ら発行した例外も、システムが投げたものと全く同じように扱われます。

プログラム内に2個所「Exception」という記述があります。これは「あらかじめ定義されているExceptionという名前の例外」という意味です。

raise文でExceptionという名前の例外を発行し、exceptブロックでExceptionという名前の例外を捕捉します。

raiseに指定した例外の名前で捕捉される

 

パート5では例外の捕捉に
「except KeyboardInterrupt:」
のように記述しました。今回は
「except Exception as e:」
例外の名前の後ろに「as e」が付いています。

 

これはexceptブロック内で、捕捉した例外に含まれる情報を使用できるように名前を付けるためです。これによりexceptブロック内では「e.args[0]」と捕捉した例外に「e」という名前でアクセスできます。

「e.args[0]」はraise文で渡されたメッセージを取得するという意味の記述です。

「as e」で捕捉した例外にアクセス

どういった際に例外を投げるのか?

例外はプログラマが自由に投げることができます。ここではどういったケースに例外を投げると良いかを解説します。

例外を投げて効率を良くできるケース

raise Exceptionによって例外を投げられることは前述の通りです。

では、どういったケースで例外を投げれば効率の良いプログラミングができるのでしょうか?

 

自ら例外を投げるケースは主に

  1. 深い階層でエラーが起こった際に即座に復帰する
  2. エラー処理を1個所に集約する

の2つが考えられます。

 

以下に「例外を投げない通常のプログラム」と「例外を投げるプログラム」を示し、その後に両者を比較します。

2本のプログラムはどちらも同じ動作を行います。

例外を投げる/投げないプログラムの処理フロー

 

どちらのプログラムも処理は

  1. 関数「sum_two_numbers()」に加算する2個の数字を渡す
  2.  受け取った2個の数字を、それぞれ関数「check_number()」に渡し確認する
  3. 関数「check_number()」内では受け取った数字が0の場合にエラーを返す

と流れます。

どちらのプログラムもエラー処理の検証が目的なので、関数「check_number()」がエラーを返すように「1.」で関数「sum_two_numbers()」に0を渡します。

 

例外を投げないPythonプログラム p6-2.py

例外を使わないプログラムの例です。関数「check_number()」が返すエラーをif文で検証しています。

#!/usr/bin/env python

import sys


# 主処理
def main():
    
    # 関数sum_two_numbers()を呼び出して数字を加算する
    result = sum_two_numbers(5, 0)
    if result == 0:
        # 異常時の処理
        print("ゼロは無効です")
        return 1
    
    # 正常時の処理
    print("sum_two_numbersの結果は{}です".format(result))
    
    return 0


# 主処理から呼び出される関数
# 引数a、bが0以外の時は両者を加算した数字を、異常時は0を返す
def sum_two_numbers(a, b):
    
    # 引数aは0か?
    if not check_number(a):
        return 0
    
    # 引数bは0か?
    if not check_number(b):
        return 0
    
    return a + b


# sum_two_numbers()から呼び出される関数
# 引数numが0の場合はFalse(異常)、その他の場合はTrue(正常)を返す
def check_number(num):
    
    # 引数は0か?
    if num == 0:
        return False
    
    return True


# プログラムの入り口
# 主処理を呼び出す
if __name__ == "__main__":
    sys.exit(main())

 

例外を投げるPythonプログラム p6-3.py

例外を使う例です。関数「check_number()」のエラーは例外として主処理のexceptブロックに捕捉されます。

#!/usr/bin/env python

import sys


# 主処理
def main():
    
    # 正常時の処理
    try:
        result = sum_two_numbers(5, 0)
        print("sum_two_numbersの結果は{}です".format(result))
    
    # 異常時の処理
    except Exception as e:
        print(e.args[0])
        return 1
    
    return 0


# 主処理から呼び出される関数
# 引数a、bが0以外の時は両者を加算した数字を返す
def sum_two_numbers(a, b):
    
    # 引数aが適切な値か確認
    check_number(a)
    
    # 引数bが適切な値か確認
    check_number(b)
    
    return a + b


# sum_two_numbers()から呼び出される関数
# 引数numが0の場合は例外を発行
def check_number(num):
    
    # 引数は0か?
    if num == 0:
        raise Exception("ゼロは無効です")


# プログラムの入り口
# 主処理を呼び出す
if __name__ == "__main__":
    sys.exit(main())

 

プログラムの説明

『例外を投げるPythonプログラム p6-3.py』では、関数「main()」、関数「sum_two_numbers()」のエラー処理にif文を使っていない点がポイントです。

 

自ら例外を投げることで
「main() ≫ sum_two_numbers() ≫ check_number()」
と深い階層を処理中にエラーが発生しても即座にmain()関数に戻れます。

 

これによって通常の処理に集中してプログラミングでき、エラー処理は主処理の終わりにまとめて記述できるので、スッキリとした見通しの良いプログラムとなります。

 

PythonのExceptionを拡張して自作の例外を投げる

ここまではException例外を投げることでプログラムの効率化を行う方法を説明しました。この例外は自作もできます。

 

なぜ例外を自作するのか?

ここまでに使用したException例外はPythonにあらかじめ組み込まれている例外です。Exception例外は汎用的な例外なのでPythonのプログラム内で一貫して使えます。

Exception例外を活用すれば深い階層からも一気に復帰でき、エラー処理を1個所に集約できます。一点、問題があるとすれば「リカバリ処理をエラーの原因に応じて切り分けにくい」点でしょうか。

 

Exception例外をエラーの原因に応じて使い分けるには引数を使用します。Exception例外には
「raise Exception(…)」
引数を与えることができます(“…”の部分)。

 

前述の例では引数に「メッセージ」を与えましたが、この部分は好きに使用できます。例外の発行時にエラー番号を渡し、捕捉時はそのエラー番号で処理を切り分けるといった使い方もできます。

ただし「引数はエラーメッセージを意味する一つの文字列だけ」がPythonの慣例なので、それに倣うのが無難です。

そこで自作した例外が必要となります。例外を自作すればPythonの慣例通り「引数はエラーメッセージ」として使用できます。

 

自作した例外を投げるPythonプログラム p6-4.py

先にPythonのプログラム例を紹介し

  •  例外を自作する
  •  例外を投げる
  •  例外を捕捉する

方法は、プログラムの後で説明します。

#!/usr/bin/env python

import sys


# `Exception`をベースに例外を作成
class OriginalException(Exception):
    pass


def main():
    
    try:
        print("主処理がスタートしました")
        
        raise OriginalException("自作の例外を投げて処理を中断!")
        
        print("主処理が終了します")
    
    # OriginalException例外を捕捉
    except OriginalException as e:
        print("例外'OriginalException'を捕捉: " + e.args[0])
    
    return 0


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

 

例外を自作する

例外の自作」と言っても難しくはありません。

Pythonのプログラム中で
「class OriginalException(Exception):」
と記述するだけです(クラスの定義)。

 

「OriginalException」が自作する例外の名前、「Exception」は作成する例外の雛形(継承元となる親クラス)です。

パート5でも説明したようにPythonの例外は階層構造になっています。詳細はPythonの公式マニュアル、例外のクラス階層を参照してください。

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

 

このようにPythonの例外はBaseExceptionから派生します。これまでに使用したException例外もBaseExceptionから派生した例外です。

また、例外を自作する際はException例外から直接派生するか、Exceptionを起点とするツリー上のいずれかの例外の雛形(親クラス)が推奨されています。

上記のプログラム「p6-4.py」はException例外から直接派生した例です。

この派生のルールを守れば例外は必要な数だけ自作できます。

 

クラスの定義に続く「pass」は「実装する処理がない」ことを意味しています。

例外を自作する方法はPythonのクラスを使用しています。あるクラスからの派生は「元となるクラスの持つ機能を全て受け継ぐ」ことを意味します。そのまま元のクラスの機能を使うこともできますし、元となるクラスの不都合な部分を改変することによって新たな機能を追加することも可能です。

自作する例外も同様です。「class OriginalException(Exception):」と記述した場合、自作した新しい例外「OriginalException」はException例外の全ての機能を引き継ぎます。もしException例外の機能に不都合があれば、クラスの定義に続いて機能を上書きするソースコードを記述すれば良いのですが、特に不都合がなければコーディングの必要はありません。Pythonでは、この「何も実装しない」を表現するのが「pass」です。

 

例外を投げる/捕捉する

自作した例外はException例外と同様の方法で発行・捕捉できます。

 

例外の発行時には
「raise 自作した例外の名前(メッセージ)」
とし、捕捉時には
「except 自作した例外の名前 as e:」
例外の名前ごとにexceptブロックを書けるので、if文などで処理を切り分ける必要がありません。

 

Pythonではこの方法が推奨されています。

 

複数の例外を自作し処理を切り分けるPythonプログラム p6-5.py

以下は複数の例外を作成して、状況によって処理を切り分けるPythonのプログラムです。

#!/usr/bin/env python

import sys


# `Exception`をベースに例外を作成
class OriginalException(Exception):
    pass
class AnotherException(Exception):
    pass

# 作成した`AnotherException`をベースに例外を作成
class E3Exception(AnotherException):
    pass
class E4Exception(AnotherException):
    pass


def main():
    
    try:
        print("主処理がスタートしました")
        
        # コメントを切り換えて作成した4個の例外を試す
        #raise OriginalException("OriginalException例外を投げて処理を中断!")
        #raise AnotherException("AnotherException例外を投げて処理を中断!")
        #raise E3Exception("E3Exception例外を投げて処理を中断!")
        raise E4Exception("E4Exception例外を投げて処理を中断!")
        
        print("主処理が終了します")
    
    # OriginalException例外を捕捉
    except OriginalException as e:
        print("例外'OriginalException'を捕捉: " + e.args[0])
    
    # AnotherException/E3Exception/E4Exception例外を捕捉
    except AnotherException as e:
        print("例外'AnotherException'を捕捉: " + e.args[0])
    
    return 0


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

自作した例外で処理が切り分けられることを確認

プログラムの冒頭で4個の例外を自作し、それぞれの例外を検証できるように例外を発行する部分をコメントアウトしています。

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

 

主処理がスタートしました
例外’AnotherException’を捕捉: E4Exception例外を投げて処理を中断!

 

例外の発行時には
「raise E4Exception(…)」
としたのに
「except AnotherException as e:」
で捕捉されています。

 

これはクラスの特徴です。

自作した4個の例外
「Exception ≫ OriginalException」
「Exception ≫ AnotherException」
「Exception ≫ AnotherException ≫ E3Exception」
「Exception ≫ AnotherException ≫ E4Exception」
と派生しています。

自作した例外の関係

E4ExceptionはAnotherExceptionの全ての機能を継承していることになるため、上記で示した結果のようにE4Exception例外がAnotherException例外として捕捉されたのです。

このように自作した例外を上手く利用することで、例外を引き起こす原因となった事象に応じた処理の切り分けが容易になります。さらにPythonのクラスの特徴を生かして「類似する事象は一括で捕捉、処理する」なども容易に行えます。

 

まとめ

ラズパイで電子機器を作成する際には、タクトスイッチなどの動きに合わせて処理を起動するのは基本中の基本です。そこで『Pythonでデバイスを制御しよう』の第2回では、ラズパイでプッシュボタンの動きに合わせて処理する方法に注目して、信号の入り口となるGPIOピンの構造や具体的な制御方法、例外と呼ばれるエラーの処理方法などを6回に分けて紹介しました。

制御でよく用いられるコーディング手法にはポーリング制御イベント駆動の2種類があり、それぞれの特徴をPythonのプログラムを作成しながら解説しました。さらに状態遷移の考えを取り入れることで、より実用的な制御が行えます。

また、例外処理に関しても受け身の対処方法から、例外を積極的に利用することで安定したシステムを構築するポイントまでを紹介しました。

単に「プッシュボタンに合わせて処理をする」と言っても電子機器の制御にはさまざまなポイントがあることをご理解いただけたと思います。紹介したポイントがヒントとなり、より安定したシステムの設計につながれば幸いです。

 

今回の連載の流れ

プッシュボタンを扱う(1) Pythonでプッシュボタンを扱うには
プッシュボタンを扱う(2) 誤動作の原因となるチャタリングを防止する
プッシュボタンを扱う(3) ポーリング制御でプッシュボタンに反応する
プッシュボタンを扱う(4) イベント駆動でプッシュボタンに反応する
プッシュボタンを扱う(5) プログラムをストップさせる例外を捕捉して処理する
プッシュボタンを扱う(6) 「raise Exception」で積極的に例外を利用する(今回)

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

https://deviceplus.jp

学生ロボコン2017 出場ロボット解剖計画