初めてのPython(27章)

例外に関連するステートメントは4つ

  1. try/except/else/finally
  2. raise
  3. assert
  4. with/as

try/except/else/finally

発生した例外を検知/処理するためのステートメント
構文とそれぞれのブロックの意味

try:
  # 例外が発生する可能性のあるステートメント
  <statement>
except [例外名[, データ]]
  # <例外名>の例外が発生した時に実行したいステートメント
  # <例外名>および<データ>は省略可能
  <statement>
else:
  # 例外が発生しなかった時に実行したいステートメント
  <statement>
finally:
  # 例外の発生有無にかかわらず実行したいステートメント
  <statement

except節は複数定義可能。<例外名>および<データ>は1つのexceptステートメントに対して複数定義可能

Yasuyuki-KOBAYASHI-no-MacBook-Pro:~ y_kobayashi$ cat exec.data 
[kobakoba0723@fedora13-intel64 ~]$ python
Python 2.6.4 (r264:75706, Apr  1 2010, 02:55:51) 
[GCC 4.4.3 20100226 (Red Hat 4.4.3-8)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> try:
...   raise IndexError, ('hoge', 'piyo')
... except (IndexError, TypeError), (data1, data2):
...   print type(data1), type(data2)
...   print data1, data2
... 
<type 'str'> <type 'str'>
hoge piyo
>>> try:
...   raise TypeError, ('hoge', 'piyo')
... except (IndexError, TypeError), (data1, data2):
...   print data1, data2
... 
hoge piyo
>>> 

あと、色々と遊んでたら不思議なことが起こった。
SyntaxErrorを発生させたときの<データ>の値がなんか変@タプルの形でデータを渡す

>>> try:
...   raise SyntaxError, ('hoge', 'piyo')
... except SyntaxError, data:
...   print data
... 
hoge (p)
>>> 
>>> try:
...   raise SyntaxError, ['hoge', 'piyo']
... except SyntaxError, data:
...   print data
... 
['hoge', 'piyo']

MacPython(2.6.1)でもおんなじ結果になった.
リファレンスでraise文を調べてみたら、

最初のオブジェクトがクラスの場合、例外の型になります。
第二のオブジェクトは、例外の値を決めるために使われます:
第二のオブジェクトがタプルの場合、クラスのコンストラクタに対する引数リストとして使われます。
このようにしてコンストラクタを呼び出して生成したインスタンスが例外の値になります。

とあった。
てことは、

>>> print SyntaxError(('hoge','piyo'))
('hoge', 'piyo')
>>> 

あら、'hoge (p)'ってならない。。。なんか勘違いしてんのかなぁ。。。

raise

例外を発生させるためのステートメントで次のようにして使う

raise
raise <name>
raise <name>, <data>

に指定できるのは、

  • ビルドイン例外
  • ユーザ定義例外(Exceptionを継承したクラスおよびそのインスタンス)

本には、文字列を代入した変数が指定できるとあったが、2.6では駄目みたい。

>>> hoge = "hoge"
>>> raise hoge
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: exceptions must be classes or instances, not str
>>> 

assert

特定条件を満たす時に例外を発生させるためのステートメント
デバッグなどプログラム開発段階で使用がメイン。
最適化するためにコンパイルするとコードが消えてしまうらしい。

機能自体は、ifステートメント + raiseステートメントで代用可能。

asssert <test>, <data>
↑は
↓と同じ
if __debug__:
  if not <test>:
    raise AssertionError, <data>

__debug__の値は、

現在の実装では、組み込み変数 __debug__ は通常の状況では True であり、
最適化がリクエストされた場合(コマンドラインオプション -O)は False 。

−O をつけると常に if の結果が False で実行されないから、そもそもコード自体を出力しないってことなのかな。

with/as

try/except/finally同様にのこと + 前処理が実現出来る。

with と コンテキストマネージャを利用することで、上記のことが出来る
コンテキストマネージャは、以下の2つの特殊メソッドを実装したクラスのインスタンス

  • __enter__
  • __exit__
with expression [as target] :
  suite

上記は次のように解釈され実行される

  1. expressionが実行されコンテキストマネージャが取得される
  2. コンテキストマネージャの__enter__()が実行される
  3. as 以降がある場合は、__enter__()の復帰値がtargetに代入される
  4. suite が実行される
  5. コンテキストマネージャの__exit__()が実行される

suite の実行中に例外が発生した場合、例外が__exit__に引数として渡される。

__exit__には、except と finallyブロックの役割をさせることが出来る。

kobakoba0723@fedora13-intel64 with_as]$ cat with_as.py 
#!/usr/bin/env python

class with_as(object):
  def __enter__(self):
    print "enter __enter__()"
  
  def __exit__(self, exec_type, exec_val, exec_tb):
    if exec_type == TypeError:
      print "Catch TypeError"
      print "enter __exit__()"
      return True
    print "enter __exit__()"
    return False

with with_as():
  print "enter block"
  raise TypeError

[kobakoba0723@fedora13-intel64 with_as]$

上と下のコードは等価と考えることが出来る

[kobakoba0723@fedora13-intel64 with_as]$ cat try_except_finally.py 
#!/usr/bin/env python

print 'enter __enter__'
try:
  print "enter block"
  raise TypeError
except TypeError:
  print "Catch TypeError"
finally:
  print "enter __exit__"

[kobakoba0723@fedora13-intel64 with_as]$

実行結果も同じになる。

[kobakoba0723@fedora13-intel64 with_as]$ python with_as.py 
enter __enter__()
enter block
Catch TypeError
enter __exit__()
[kobakoba0723@fedora13-intel64 with_as]$ python try_except_finally.py 
enter __enter__
enter block
Catch TypeError
enter __exit__
[kobakoba0723@fedora13-intel64 with_as]$ 

でもって、__exit__の中で定義していない例外を上げてやると、
exceptに指定しなかった例外が発生したとき同様に例外が表に現れる。

[kobakoba0723@fedora13-intel64 with_as]$ python with_as.py
enter __enter__()
enter block
enter __exit__()
Traceback (most recent call last):
  File "with_as.py", line 18, in <module>
    raise IndexError
IndexError
[kobakoba0723@fedora13-intel64 with_as]$ 

__exit__の外に例外を出すか出さないかは、__exit__の復帰値で制御する。
Trueの場合は外に出さない、Falseの場合は外に出す。

with/as を使うとコードが少なくて済むのがファイルのアクセス。
try/except/finallyを使う場合だと以下のようになるが、

try:
  f = open(file_name, mode)
  for line in f:
    print line.strip()
except IOError:
  print "file can not open"
finally:
  f.close()

with/asを使うとファイルのオープン/クローズを自動でやってくれるので、これだけで済んじゃう。

with open(file_name, mode) as f:
  for line in f:
    print line.strip()

ということで早速確認

[kobakoba0723@fedora13-intel64 with_as]$ cat open_file.py 
#!/usr/bin/env python

with open('hoge.data', 'r') as f:
  for line in f:
    print line.strip()
print f

[kobakoba0723@fedora13-intel64 with_as]$ ls
hoge.data  open_file.py
[kobakoba0723@fedora13-intel64 with_as]$ python open_file.py 
hoge
piyo
egg
spam
<closed file 'hoge.data', mode 'r' at 0x7f05cd240a48>
[kobakoba0723@fedora13-intel64 with_as]$ mv hoge.data piyo.data
[kobakoba0723@fedora13-intel64 with_as]$ python open_file.py 
Traceback (most recent call last):
  File "open_file.py", line 3, in <module>
    with open('hoge.data', 'r') as f:
IOError: [Errno 2] No such file or directory: 'hoge.data'
[kobakoba0723@fedora13-intel64 with_as]$ 

うん、自動的にファイルの開け閉めしてくれて、エラー処理もしてくれてる。
で、ファイルが無かったときにプログラムを止めないようにするには、
このモジュールの外側でIOErrorを拾ってやればよいってことになる。

以下のようなコンテキストマネージャを返すクラスを作って、
__exit__でIOErrorを受け取れば、モジュールの外側でごにょごにょしなくていいんじゃない?
なんておもったけどだめでした。そんなにあまかぁない。

[kobakoba0723@fedora13-intel64 with_as]$ cat open_file_myself.py 
#!/usr/bin/env python

class openFile(object):
  def __init__(self, file_name, file_attr):
    self.file = file_name
    self.attr = file_attr
    
  def __enter__(self):
    self.fd = open(self.file, self.attr)
    return self.fd
  
  def __exit__(self, exec_type, exec_val, exec_tb):
    retval = False
    if exec_type == None:
      retval = True
    elif exec_type == IOError:
      print 'file cannot open'
      retval = True
    self.fd.close()
    return retval

with openFile('hoge.data', 'r') as f:
  for line in f:
    print line.strip()
print f

[kobakoba0723@fedora13-intel64 with_as]$ ls
hoge.data  open_file_myself.py
[kobakoba0723@fedora13-intel64 with_as]$ python open_file_myself.py 
hoge
piyo
egg
spam
<closed file 'hoge.data', mode 'r' at 0x7f25afea4a48>
[kobakoba0723@fedora13-intel64 with_as]$ mv hoge.data piyo.data
[kobakoba0723@fedora13-intel64 with_as]$ python open_file_myself.py 
Traceback (most recent call last):
  File "open_file_myself.py", line 22, in <module>
    with openFile('hoge.data', 'r') as f:
  File "open_file_myself.py", line 9, in __enter__
    self.fd = open(self.file, self.attr)
IOError: [Errno 2] No such file or directory: 'hoge.data'
[kobakoba0723@fedora13-intel64 with_as]$ 

正常系の場合はファイルの開け閉めしてくれていいんだけど、エラー系は思った通りに動かない。
というよりもそもそも無理なことをしようとしてたみたい。
ドキュメントによると、

with 文は、 __enter__() メソッドがエラーなく終了した場合には __exit__() が常に呼ばれることを保証します。
もしエラーがターゲットリストへの代入中にエラーが発生した場合には、これはそのスイートの中で発生したエラーと同じように扱われます。

ってことで、__enter__()でエラーが出た場合は、__exit__()が呼ばれる保証はどこにもない。
なので__exit__()でIOErrorを拾ってやろうとしても拾えるもんじゃない。
__exit__()で拾えるのは、

  • openFile('hoge.data', 'r')で生成されるインスタンスをfに代入するときのエラー(例外)
  • for ループの中の処理で発生したエラー(例外)

それ以外の時は、外側に例外が出てしまう。
まぁ、ファイルオープンが出来ない状態でプログラムを続ける用途がどれぐらいあるかって考えたら、
この状態でも問題はない気がしてきた。