Pythonの例外処理に関するまとめ

適当に書いてしまいがちな例外処理について自分なりにまとめました。

Pythonにおける例外

Pythonでは「認可をとるより許しを請う方が容易 (easier to ask for forgiveness than permission)」、略してEAFPというコーディングスタイルが推奨されています。EAFPは、エラーを起こすかもしれない処理もまず実行してみて、もしエラーが出たらそのとき後始末をする、というスタイルです。従って、Pythonは比較的例外処理が使われやすい言語であると言えます。

基本的な文法

0除算エラーと型エラーを捕捉する例を以下に示します。

def func(val):
    try:
        res = 1 / val
    except ZeroDivisionError:
        print("zero division error")
    except TypeError:
        print("type error")
    else:
        print("1 / {} = {}.".format(val, res))
    finally:
        print("end of func()")

func(1)
func(0)
func(None)

各節の意味は以下の通りです。

  • try節
    • 例外が発生する可能性があるコードを書くところです。
  • except節
    • 例外クラス名を指定し、捕捉する例外の種類を限定して捕捉します。何個も並べることもできます。
  • else節
    • try節内で例外が発生しなかった場合のみ実行される節です。
  • finally節
    • try節内で例外が発生したか否かに関わらず実行される節です。

実行結果は以下です。

1 / 1 = 1.0.
end of func()
zero division error
end of func()
type error
end of func()

例外オブジェクトを捕捉するにはasを使います。

try:
    1/0
except ZeroDivisionError as e:
    print(e)

実行結果は以下です。

division by zero

すべての例外を捕捉する

発生する例外の種類がわかっているときは、前節で書いたように、なるべく具体的に例外の種類を指定するのがよいとされています(要出典)。しかし実際には、どんな例外を発生するかわからないコードを呼び出す必要に迫られることがあります。このときには発生しうるすべての例外を捕捉したくなるでしょう。よく見かけるがあまりお勧めでない書き方を以下に示します。

try:
    dirty_func()
except:
    print("error!")

この書き方には2つの問題があると言えます。

  1. 捕捉したくない例外まで捕捉されてしまう
  2. 重要なエラーを揉み消してしまう

順番に見ていきます。

問題1. 捕捉したくない例外まで捕捉されてしまう

このコードの1つ目の問題は、Ctrl+CによるKeyboardInterruptまでをも捕捉してしまうことです(もちろん、それが意図したことならそれでよいのですが)。例えば以下のコード

sum = 0
while True:
    try:
        sum += 1
    except:
        print("error!")

を実行し、Ctrl+Cを押してみてください。ちょうどsum += 1の実行中にCtrl+Cを押すとそれが例外として捕捉されてしまうので、複数回Ctrl+Cを押さないと終了できないことがあると思います。

改善したコードを以下に示します。こうすると、KeyboardInterruptSystemExitなど捕捉したくないいくつかの例外を除いた(ほぼ)すべての例外が捕捉できます。

try:
    dirty_func()
except Exception as e:
    print(e)

この理由は5. 組み込み例外 — Python 3.5.2 ドキュメントを見ると分かります。このページのExceptionクラスの説明には「システム終了以外の全ての組み込み例外はこのクラスから派生しています。全てのユーザ定義例外もこのクラスから派生させるべきです」とあります。つまり、まともなライブラリが返す例外は、すえてExceptionクラスを継承していることが期待されるので、上記のコードでうまくいくわけです。

ただし、可能性は低いですが、Exceptionクラスを継承せずに作成した行儀の悪いユーザ定義例外を返されてしまうと、上記コードでは捕捉できません。例を以下に示します。

# 誤った例外クラス定義の方法! 正しくはExceptionクラスを継承してください
class MyError(BaseException):
    pass

try:
    raise MyError
except Exception as e:
    print("error!")

これを実行するとエラー捕捉に失敗することが確かめられます。このようなレアな現象まで考慮せざるを得ない場合は、except Exception:ではなくexcept:と書かないといけないはずです。

問題2. 重要なエラーを揉み消してしまう

例外をキャッチして適切な処理をせずそのまま揉み消すくらいなら何もしない方がマシなことがあります。例えば以下のコードは0から9までの和を計算するつもりのコードですが、実行すると0が表示されます。

ans = 0
for i in range(10):
    try:
        ams += i
    except Exception:
        pass
print(ans)

これは、ansをミススペルしたamsによりNameErrorが発生したのにも関わらず、それを揉み消してしまったせいで起こった悲劇です。プログラムは動作し続けるものの、どんな挙動になるか予想ができない危険な状態と言えます。

改善案として、except節にてraiseにより例外を投げ直すという手があります。コードを以下に示します。

ans = 0
for i in range(10):
    try:
        ams += i
    except Exception:
        raise
print(ans)

これを実行すると、以下のように例外が出てプログラムが停止します。

Traceback (most recent call last):
  File "error_test.py", line 9, in <module>
    ams += i
NameError: name 'ams' is not defined

このように、exceptチェーンの最後ではraiseと書いておくのがよいと思います。

どんな怪しい状態になってもいいからどにかくプログラムを停止させたくない、という場合ではraiseと書きたくないかもしれません。このような場合であっても、最低限なんらかのログを残しておくことくらいはしておくべきだと思います。

スタックトレースを表示

例外処理をするとスタックトレースが表示されなくなってしまうので、デバッグのときに不便です。例えば以下のコードを実行すると、division by zeroとしか表示されず、エラー箇所がわかりません。

def func1():
    1 / 0

def func2():
    return func1()

try:
    func2()
except Exception as e:
    print(e)

スタックトレースを表示するには、標準ライブラリのtracebackを使います。

import traceback

def func1():
    1 / 0

def func2():
    return func1()

try:
    func2()
except Exception as e:
    traceback.print_exc()

上記コードを実行すると、以下のように例外発生箇所が分かります。

Traceback (most recent call last):
  File "error_test.py", line 26, in <module>
    func2()
  File "error_test.py", line 23, in func2
    return func1()
  File "error_test.py", line 20, in func1
    1 / 0
ZeroDivisionError: division by zero

参考URLs

Pythonのvars()とdir()の違い

よくどっちがどっちだったか混乱してしまうので自分用にまとめます。最初に、引数にオブジェクトを渡して呼び出したときの挙動を比較したのち、引数なしで呼び出したときの挙動を比較します。

vars(obj)

2. 組み込み関数 — Python 3.5.2 ドキュメントによると

モジュール、クラス、インスタンス、あるいはそれ以外の dict 属性を持つオブジェクトの、 dict 属性を返します

とあります。例で確かめてみます。

class MyClass:
    def __init__(self):
        self.val1 = 10
        self.val2 = 20

obj = MyClass()
print(vars(obj))
print(obj.__dict__)

この出力は以下の通りです。vars(obj)は、objを辞書として扱ったときの値を返していると解釈できそうです。

{'val1': 10, 'val2': 20}
{'val1': 10, 'val2': 20}

dir(obj)

2. 組み込み関数 — Python 3.5.2 ドキュメントによると

そのオブジェクトの有効な属性のリストを返そうと試みます

とあります。以下は、先ほどのvars()をdir()で置換しただけのコードです。

class MyClass:
    def __init__(self):
        self.val1 = 10
        self.val2 = 20

obj = MyClass()
print(dir(obj))

この出力は以下の通りです。

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'val1', 'val2']

このように、オブジェクトが持つ属性のみならず、オブジェクトが属しているクラスが持つ属性をも含んだリストが返ってきます。dir()関数の使用用途としては、プログラマが対話的にオブジェクトが持つ属性の一覧を覗いて、使えそうなものを探ることのようです。再び2. 組み込み関数 — Python 3.5.2 ドキュメントから引用します。

注釈 dir() は主に対話プロンプトでの使用に便利なように提供されているので、厳密性や一貫性を重視して定義された名前のセットというよりも、むしろ興味を引くような名前のセットを返そうとしま す。また、この関数の細かい動作はリリース間で変わる可能性があります。例えば、引数がクラスであるとき、メタクラス属性は結果のリストに含まれません。

vars()

引数なしでvars()を呼ぶと、「locals() のように振る舞います」とのこと。locals()は、「現在のローカルシンボルテーブルを表す辞書を更新して返します」とのこと。以下のコード

class MyClass:
    def __init__(self):
        self.val1 = 10
        self.val2 = 20

obj = MyClass()
print(vars())

の実行例は以下です。

{'__spec__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x00000289CD407940>, '__file__': '/path/to/somewhere.py', '__cached__': None, '__builtins__': <module 'builtins' (built-in)>, '__name__': '__main__', 'obj': <__main__.MyClass object at 0x00000289CD4698D0>, 'MyClass': <class '__main__.MyClass'>, '__doc__': None, '__package__': None}

dir()

引数なしでvars()を呼ぶと、「現在のローカルスコープにある名前のリストを返します。」とのこと。

class MyClass:
    def __init__(self):
        self.val1 = 10
        self.val2 = 20

obj = MyClass()
print(dir())

の実行例は以下です。

['MyClass', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'obj']

引数なしvars()の戻り値である辞書のkeys()と同じ結果のように見えます。

参考URL

PyCharmでエディタの文字サイズをCtrl + マウスホイールで変更可能にする

表題のようにする方法が PyCharm 2016.1 Help :: Zooming in the Editor にあります。 File -> Settings...と辿ってウィンドウを開いた後、左側のペインでEditor -> Generalと辿り、"Change font size (Zoom) with Ctrl+MouseWheel"にチェックを付ければOKです。 PyCharm Community Edition 2016.1 で確認しました。

pythonでwithによるネストを防ぐ

Pythonではファイルを開くときなどにwithを使うのが定石である(参考:ファイル - Dive Into Python 3 日本語版)。しかし、複数のファイルを開くときには以下のようにネストが発生してしまうのが気に入らなかった。

with open('a.txt', 'w') as f1:
    with open('b.txt', 'w') as f2:
        # 処理

Multiple variables in Python 'with' statement - Stack Overflow によると、以下のコードでネストを回避できる。Python 2.7とPython 3.1以降で利用可能らしい。

with open('a.txt', 'w') as f1, open('b.txt', 'w') as f2:
    # 処理

O'Reillyで買ったebooksをKindleで読む

現在O'ReillyでEbookがすべて半額セール中 (Ebooks - O'Reilly Media)なのを利用して、C in a Nutshell, 2nd Edition - O'Reilly Mediaを買ってみました。いつも購入したEbookをスマホで読めるようにする方法を忘れてしまうので、簡単にメモします。

管理画面

EbookをクレジットカードやPaypalで購入した後、"Your Account"、"Your Products"と順にクリックして購入したEbookを探す。

f:id:minus9d:20160912205345p:plain

Dropboxとの同期

"Send Ebook"、"Send To Dropbox"を順にクリックしてDropboxと紐付けることにより、Ebookが更新されたときに自動でDropbox上のEbookも更新される。

Kindleへの転送

Kindleで使える無料の英和辞書が素晴らしいため、Kindleで読むのが自分にとってはもっとも効率がよい。Amazonから"Send-to-Kindle Eメールアドレス"というのをもらっているはずなので、そのアドレスに.mobiファイルを添付して送るだけでよい。自分の"Send-to-Kindle Eメールアドレス"の探し方は、Amazon.co.jp ヘルプ: Send-to-Kindle Eメールアドレスの使い方についての「Send-to-Kindle Eメールアドレスの表示または変更」を参照。

ピタゴラス数を無限に生成する

先日久しぶりに参加したCodeforces Round #368 (Div. 2) - Codeforcesの C. Pythagorean Triples が面白かったのでご紹介します。

問題

「正の整数aが与えられる。三平方の定理 a2 + b2 = c2 を満たす残りの 正の整数b, cの組を一組答えよ。そのようなb, cの組が存在しない場合は-1と出力せよ」

解答

少し考えてわからなかったのでググったところ、Pythagorean triple - Wikipedia, the free encyclopediaの"The Platonic sequence"に答がありました。これによると、

  • aが奇数の場合:b = (a2 - 1)/2, c = (a2 + 1)/2
  • aが偶数の場合:b = (a/2)2 - 1, c = (a/2)2 + 1

とすれば、三平方の定理を満たすピタゴラス数を生成できます。ただしaは3以上です。

導出

なぜこの方法でピタゴラス数を生成できるのかを考えてみます。まず、a2 + b2 = c2 を変形すると、a2 = (c - b) (c + b) となります。ここで、c - b, c + bは必ず偶奇が一致することに着目し、a2の偶奇に従って場合分けをします。

aが奇数の場合

c - b, c + bはともに奇数である必要があります。c - b = 1, c + b = a2 とするのが簡単で、連立方程式を解くと先に示した式になります。

aが偶数の場合

c - b, c + bはともに偶数である必要があります。c - b = 2, c + b = a2 / 2とするのが簡単で、連立方程式を解くと先に示した式になります。

注意

この方法ではすべてのピタゴラス数を列挙できるわけではないことに注意が必要です。例えば、(20, 21, 29)や(28, 45, 53)はこの方法では生成できません。

PyInstallerでPythonスクリプトをexe化

PythonスクリプトをWindowsのexeにする方法 (調査中) - minus9d's diary にて、Python 3.5のスクリプトWindowsのexe化するにはPyInstallerが良さそうだという記事を書きました。この記事ではPyInstallerを使ってexe化する方法について調査した結果を記します。

スクリプトの用意

数式を微分するスクリプト differentiator.pyを用意します。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from sympy import symbols, diff
from sympy.parsing.sympy_parser import parse_expr

while True:
    x = symbols('x')
    math_expr = input('please input f(x): ')
    print("f'(x) =", diff(parse_expr(math_expr), x))

このスクリプトをコンソールで実行すると

please input f(x):

と聞かれるので、

a * x * sin(x)

などと入力してください。f(x)をxで微分した結果が以下のように得られるはずです。

f'(x) = a*x*cos(x) + a*sin(x)

Ctrl + Cで終了です。このスクリプトをexe化することを目標とします。

環境の用意

Windows 10 + Anaconda Python 64bit 4.1.6 + Python 3.5.1にて

> pip install pyinstaller

としてpyinstallerをインストールしました。現時点で3.2がインストールされました。

exeは生成できるが実行できない

exeを生成するには、コマンドプロンプトにて

> pyinstaller --onefile differentiator.py

とすればOKなはずです。実行すると、大量のWARNINGとErrorが出るものの、dist以下にdifferentiator.exeが生成されました。

しかし、生成されたexeをダブルクリックすると

---------------------------
differentiator.exe - 正しくないイメージ
---------------------------
C:\(省略)\VCRUNTIME140.dll は Windows 上では実行できないか、エラーを含んでいます。元のインストール メディアを使用して再インストールするか、システム管理者またはソフトウェアの製造元に問い合わせてください。エラー状態 0xc000007b。 
---------------------------
OK   
---------------------------

というエラーダイアログが出て、実行に失敗しました。

setuptoolsをダウングレードしてexeの実行に成功

'six' is required; worked fine with previous version of pyinstaller · Issue #1773 · pyinstaller/pyinstaller · GitHubに、setuptoolsのバージョンを19.2に下げると解決したという報告があります。私の環境でも、setuptoolsのバージョンを23.0.0から19.2に下げると、確かにexeが実行できるようになりました。ただし、exeの生成時に大量のWARNINGとErrorが出るのは相変わらずなので、まだ問題が潜んでいる可能性はあります。

以下は、Anacondaで仮想環境を作って試したときのコマンドです。仮想環境の詳細については"conda create"でググってください。

> conda create -n pyinstaller_test python setuptools=19.2 sympy pywin32
> activate pyinstaller_test
> pip install pyinstaller