contextmanager



内部DSLパターン

でも書きましたが、Pythonでもwith文を使えば、Rubyのブロックと似たような事が出来ます。


しかし、with文コンテキストマネージャには、__enter__、__exit__メソッドを定義しなければならないのが面倒です。
そこで、contextlib.contextmanagerデコーレータを使います。



from __future__ import with_statement
from contextlib import contextmanager

import time
from datetime import datetime

@contextmanager
def stopwatch():
    print "計測開始 %s"%(datetime.now())
    begin = time.clock()
    
    try:
        yield
    finally: 
        end   = time.clock()
        print "計測終了 %s"%(datetime.now())
        print "所要時間 = %s"%(end - begin)
    
def fib(n):
    #計算に時間がかかる関数
    if n == 0 or n == 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

with stopwatch():
    for i in xrange(30):
        print i, fib(i)


contextmanagerデコレータを使える条件は、yieldが1回だけのジェネレータ関数であること。


0回でも2回でもダメ。


今回の例ではyieldが返す値は空ですが、何らかの値を返すことも出来て、
その値が、with spam() as egg で egg に代入されます。



ブロックの実行中に例外が投げられると、yieldの書かれた所に再送出されます。


つまり、ブロック実行後の処理(今回はprint "所要時間 = %s"%(end - begin)等)を確実に実行させるためには、
try 〜 finally 〜でくくる必要があります。


もちろん、try〜except〜で、例外を補足したり、再送出する事もできます。





ちなみに、contextlibモジュールには、nestedとclosingという関数も用意されています。



closing() は、単にブロックの終了時にcloseメソッドを呼びます。

from __future__ import with_statement
from contextlib import closing

with closing(urllib.urlopen('http://www.python.org')) as page:
    for line in page:
        print line

# これと同等
page = closing(urllib.urlopen('http://www.python.org')
try:
    for line in page:
        print line
finally:
    page.close()



nested は with に コンテキストマネージャ を複数指定したいときに使います。

from contextlib import nested

with nested(A, B, C) as (X, Y, Z):
    do_something()


#これと同等
with A as X:
    with B as Y:
        with C as Z:
            do_something()


ちなみにpython 3.1 では、新しい文法が追加されてnestedは非推奨になっています。
python 2.7でも使えるのかは知りませんが、2.7はまだα版みたいです。

with open('mylog.txt') as infile, open('a.out', 'w') as outfile:
    for line in infile:
        if '<critical>' in line:
            outfile.write(line)