読者です 読者をやめる 読者になる 読者になる

ローグウェーブソフトウェアのブログ

開発をシンプルに 安全で高品質のコードを 素早くお客様のもとへ

Python コーディング tips #1: with文

プログラミング

ローグウェーブ本社のブログ、CodeBuzzにローグウェーブのTrevor Reidが投稿した、Pythonについての記事をご紹介します。

Python coding tips #1: with statements

blog.klocwork.com

Pythonのもっと知られてよい素晴らしい機能のひとつにwith文があります。公式文書(英語日本語。どちらも2系)。

with 文は、ブロックの実行を、コンテキストマネージャによって定義されたメソッドでラップするために使われます( swith文とコンテキストマネージャ セクションを参照してください)。これにより、よくある try...except...finally 利用パターンをカプセル化して便利に再利用することができます。

ここで述べられているように、with文は広く使われているtry/except/finally パターンをわかりやすいコードに置き換えるものです。Pythonで最もよく使われる例はファイルハンドラです。

最も一般的な使用例: ファイルハンドラ

Pythonでファイルを操作する場合、コードは以下のステップに分解されます。 1. ファイルをopen() 2. ファイルに対して処理 3. 開いたファイルをclose()

例えば

def read_log_file(self, test_file):
    f = open(test_file, "r")
    lines = f.readlines()
    f.close()
 
    return lines

とてもシンプルですが、これをさらに

def read_log_file(self, test_file):
    with open(test_file, "r") as f:
        lines = f.readlines()

    return lines

この場合大きな変化はありませんが、withを使うとf.close()を呼び出してくれるため、withブロックからどのように終了してもファイルがクローズされることが保証されます。上の例は以下と同値です。

def read_log_file(self, test_file):
    try:
        f = open(test_file, "r")
        lines = f.readlines()
    except:
        raise Exception
    finally:
        f.close()
    return lines 

ここでも主なメリットはファイルが自動的にクローズされるため、ファイルを明示的に閉じる必要なく、またうっかり忘れてしまう心配もありません。

どのような仕組みなのか?

with文を利用するには、クラスには__enter__()と__exit__()が実装されていなければなりません。withが呼び出されると、クラスの__enter__()メソッドが呼び出され、withブロックが終了する際には__exit__()メソッドが呼び出されます。クラスにこれらのメソッドを実装すると、withのメリットを享受でき、コーディングの手間を軽減させることができます。例として、ビルドサーバからインストーラをコピーしてくるクラスを考えてみましょう。

コピークラスは以下のように定義できます。

import shutil
import os
import tempfile
 
class CopyInstallers(object):
 
    SERVER_INSTALLER = "vncserver.exe"
    VIEWER_INSTALLER = "vncviewer.exe"
    def __init__(self, version):
        self.version = version
        self.temp_installer_dir = tempfile.mkdtemp()
        self.installer_list = list()
    
    def __enter__(self):
        self.copy_from_server()
        return self
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        shutil.rmtree(self.temp_installer_dir)
 
    def copy_from_server(self):
        build_server = r"\\BUILDSERVER\release\{0}\{1}"
        for installer in [SERVER_INSTALLER, VIEWER_INSTALLER]:
            shutil.copy(build_server.format(self.version, installer), self.temp_installer_dir)
            self.installer_list.append(os.path.join(self.temp_installer_dir, installer))  

また、このクラスを以下のようにテストできます。

import subprocess
from copy_installer import CopyInstallers
 
 
class Install(object):
    def install(self, product, version):
        with CopyInstallers(installer=product, version=version) as copy:
            for installer in copy.installer_list:
                subprocess.call("install {0}".format(installer), shell=True)

install()メソッドはCopyInstallers クラスを生成し、withキーワードで.exeをサーバからローカルの一時ディレクトリへとコピーします。with文が終了したら、一時ディレクトリは消去されます。__exit__()メソッドでは、必要なら例外をキャッチしてエラーハンドリング処理を書くこともできます。

asという構文について、このas節は__enter__()メソッドが返すものを、それがなんであれ返します。上記の例ではCopyInstallers オブジェクトを生成し、そのオブジェクトへの参照をwith本体内部で使いたい場合、__enter__()がselfへの参照を返すだけでよいのです。もしwithキーワードの後で参照が必要ないのでしたら、as節を省略しても構いません。

付記として、これらは__init__()や__del__()の呼び出しを置き換えるものではありません。 __init__()は__enter__()の前に実行され、__del__()は__exit__()の後に実行されます。

既知の問題

__enter__()内メソッド内で例外が投げられると、__exit__()が呼ばれません。 try/exceptブロクを__enter__()内に書いてexcept/finallyブロック内で__exit__()を呼び出すか、その他何らかの方法で例外を処理する必要があります。

TREVOR REID

ご意見ご要望

今回のpythonに関する記事はいかがだったでしょうか。最後の既知の問題については、C++でもコンストラクタ/デストラクタを使ったリソースの確保/破棄に関して同様の問題があります。スコット・メイヤーズによるMore Effective C++には、「コンストラクタでのリソースリークを防ぐ」という項目で、特にコンストラクタ内で複数のリソースを動的確保したときにリソースリークを防ぐためのコードの晦渋さをどう扱うか、詳細に論じられています。動的確保したメモリの管理については「スマートポインタを使ってイニシアライザ内で生成すべし」と結論付けられていますが、今回の例のようにある程度複雑な事例になってくると、軽量言語風にきれいにコードを書くことと例外安全なコードを書くことの間に、ジレンマが生じてくるように感じます。とはいえ今回ご紹介したこのwith文はかなりの程度その両立に貢献してくれることは間違いないでしょう。

ローグウェーブのブログでは今後もプログラミングや開発手法に関する記事をご紹介していきます。ご意見やご要望がありましたらお気軽にローグウェーブまでご連絡ください。

ローグウェーブ セールスエンジニア 柄澤(からさわ)