ラベル with文 の投稿を表示しています。 すべての投稿を表示
ラベル with文 の投稿を表示しています。 すべての投稿を表示

2015年12月15日

with文の話3

with文専用のオブジェクトをより簡単に定義する contextlib.contextmanager を紹介します。

こちら で、print をハックする with文専用クラスを紹介しました。
このクラスを中身を見てみると、__enter__() と __exit__() という2つのメソッドが定義されているのみです。このようなクラスと同じ動作をする関数を作れるようにするのが contextlib.contextmanager です。 contextlib.contextmanager を使った実装例は以下のようになります。

import sys
import io
from contextlib import contextmanager

@contextmanager
def print_hack():
    sio = io.StringIO()
    tmp, sys.stdout = sys.stdout, sio
    yield sio
    sys.stdout = tmp
行なっていることは以前のクラスと同じです。変わった点は
  • クラスから関数になった
  • contextmanager のデコレータを使用する
  • __enter__() と __exit__() に相当する部分が yield で区切られる。yield までが __enter__()、yield より後ろが __exit__()、となる
となります。


関数の使い方は以下の通りで、以前のクラスと全く同じです。

# with文でprint_hack()を呼び出す
>>> with print_hack() as hack:
...     print('Hello hack')
...
>>>   # ここではprintされない

# with内のprintの内容を確認
>>> hack.getvalue()
'Hello hack\n'

また、ここで定義した関数は通常の関数呼び出しからは呼び出すことはできません。誤使用を防ぐという意味で、大事な点です。
>>> print_hack()
<contextlib._GeneratorContextManager object at 0x01780470>


個人的には、クラスを定義すると self をたくさん書かなければならない点が苦痛です。関数で済むなら関数で済ます方が楽で良いのではないでしょうか。

2015年12月14日

with文の話2

with文での呼び出しを前提としたクラスの実装例を挙げます。

以下のクラスは、print(正確には sys.stdout)に送られてくる文字列を別 IO に取り込むクラスです。クラスは __enter__() と __exit__() のみから成り、with文で使われることが前提となっています。

import sys
import io

class PrintHack(object):
    def __enter__(self):
        # sys.stdoutをio.StringIOに置き換え
        self.__sio = io.StringIO()
        self.__tmp, sys.stdout = sys.stdout, self.__sio

        return self.__sio

    def __exit__(self, exc_type, exc_value, traceback):
        # sys.stdoutを元に戻す
        sys.stdout = self.__tmp

使い方は以下のようになります。
まず with文で PrintHack クラスを呼び出します。すると、with のスコープの中で呼ばれた print の内容は、標準出力には出力されず、PrintHack が持つ IO に取り込まれるようになります。取り込まれた内容は、IO の getvalue() で確認することができます。
このクラスの主な使い道は、テストプログラムにおいて標準出力の内容を確認する、となるはずです。
# with文でPrintHackを呼び出す
>>> with PrintHack() as hack:
...     print('Hello hack')
...
>>>   # ここではprintされない

# with内のprintの内容を確認
>>> hack.getvalue()
'Hello hack\n'

# with外では通常のprint
>>> print('Not hacked')
Not hacked

sys.stdout は文字通り標準の出力ですので、基本的にはデフォルトから変更を加えたくないものです。sys.stdout に加えた変更が間違って残ってしまうと、プログラムの他の箇所で予期せぬ問題が発生する恐れがあります。
with文専用のクラスを作ることで、影響範囲を with スコープの中に限定することができました。これは堅牢性の観点から非常に重要です。
また、仮にこのクラスが with文以外で呼ばれた場合、このクラスは何の動作もしません。with文以外では動作しないようにすることで、間違った使い方を未然に防ぐことができます。

2015年12月13日

with文の話1

Python の with文の基本的な事柄についてお話します。

ファイルの open 処理に with文が使われるようになって久しいです。
改めて、with文の役割とは何でしょうか?
大雑把に言うと、1つ目のソースコードと2つ目のソースコードはほぼ同じ動きをします。

>>> with open(filename) as file
...     data = file.read()
>>> file = open(filename)
>>> try:
...     file = file.__enter__()
...     data = file.read()
... finally:
...     file.__exit__()
with文を使うことで、いちいち try - finally を書く必要がなくなり、プログラムの見通しがずっと良くなります。特に try と finally との間が長くなってしまう場合、可読性の点での恩恵は大きいです。

また、__exit__() は with のスコープを出る直前に呼ばれることが保証されています。__del__() が呼ばれるタイミングは Python の実装依存ですので、with + __exit__() の組み合わせはより制御しやすい動作であると言えます。

さらに、__enter__() と __exit__() の呼び出しが保証されることは、with文で呼ばれるオブジェクトにとって、非常に重要な意味を持ちます。
「with文で呼ばなければ何も動作しないクラス」を定義することで、クラスの正しい使い方を強制することができます。これは、「このクラスの open() と close() は必ずペアで呼び出して下さい」とコメントで書くよりも遥かに安全なクラスの書き方となります。

次項にて with文で動作するクラスの具体的な例を示します。