ZipFileの生存期間とyield

zip ファイルに格納されたオブジェクトを、その名前とデータのタプルの列に変換して、後で処理できるようにしたい、とおもってですね。イテレーターオブジェクトにしておけば、複数の zip ファイルを itertools.chain でつないで別の処理に回すことができるじゃないかって、まあそんなことを考えたわけです。

結論から言えば、 yield を使ってこう書けばよい。

def iter_from(filename):
  from zipfile import ZipFile as openZip
  with openZip(filename) as zipf:
    for name in zipf.namelist():
      if not name.endswith('/'):
        yield (name, zipf.open(name))

上を使って、例えば以下のような処理が書ける:

for (name, data) in iter_from('hoge.zip'):
  print name, data.read()

や、面白みはないけれど、ちゃんと zip の中のファイルだけ取り出して中身を読み出せる。 iter_from の実装では with 構文を使っているので、最後まで処理すれば openZip で開いたファイルも閉じられる!

いくつかの苦労

最初はこんな実装をしたんですよ。

def iter_from(filename):
  from zipfile import ZipFile as openZip
  with openZip(filename) as zipf:
    return ((name, zipf.open(name)) for name in zipf.namelist() if not name.endswith('/'))

いつぞや内田さんに教えてもらった generator 構文を使って、いやあ、 python ってスッキリ書けて素晴らしいなあ、などと鼻歌まじりに。
そしたらですね、 with の中で return しているのでスコープを抜けて zipf が閉じられてしまうわけですよ。参照するとエラーが起きちゃうわけですよ。閉じた ZipFile を open すること、まかりならず! って。

仕方がないなあ…… じゃあ、 zipfile 開き直すのはどうにも癪だけど、こうか?

def iter_from(filename):
  from zipfile import ZipFile as openZip

  def _aux(name):
    from cStringIO import StringIO
    with openZip(filename) as zipf:
      return StringIO(zipf.open(name).read())

  with openZip(filename) as zipf:
    return ((name, _aux(name)) for name in zipf.namelist() if not name.endswith('/'))

まあこれなら、冒頭の実装のように動くことは動く。ただタプルの要素を参照するたびに、再度 zipfile を作成し直さないといけないってのがどうにもイケてないな、と。

で、さらに色々考えて冒頭の yield 使ったバージョンにたどり着いたという次第。

ジェネレーター構文による簡潔さと比べるとあまりに拙いけれど、スコープとオブジェクトの生存期間とがうまくミックスしているあたりがわたくし好みです。