14

私は最近、開いているファイルのシーケンスを返すメソッドを書きました。つまり、次のようなものです。

# this is very much simplified, of course
# the actual code returns file-like objects, not necessarily files
def _iterdir(self, *path):
    dr = os.path.join(*path)
    paths = imap(lambda fn: os.path.join(dr, fn), os.listdir(dr))

    return imap(open, paths)

構文的には、次のようなことを行った場合、結果のオブジェクトを閉じる必要はないと思います。

for f in _iterdir('/', 'usr'):
    make_unicorns_from(f)
    # ! f.close()

その結果、_iterdirコンテキスト マネージャーでラップすることにしました。

def iterdir(self, *path):
    it = self._iterdir(*path)

    while 1:
        with it.next() as f:
            yield f

これは正しく機能しているようです。

私が興味を持っているのは、これを行うことが良い習慣であるかどうかです。このパターンに従って問題が発生することはありますか (おそらく例外がスローされた場合)?

4

2 に答える 2

8

私が見る2つの問題があります。1 つは、一度に複数のファイルを使用しようとすると、問題が発生することです。

list(iterdir('/', 'usr')) # Doesn't work; they're all closed.

2 番目は CPython で発生する可能性は低いですが、参照サイクルがある場合、またはコードが別の Python 実装で実行される場合、問題が明らかになる可能性があります。

で例外が発生した場合make_unicorns_from(f):

for f in iterdir('/', 'usr'):
    make_unicorns_from(f) # Uh oh, not enough biomass.

使用していたファイルは、ジェネレーターがガベージ コレクションされるまで閉じられません。その時点で、ジェネレーターのcloseメソッドが呼び出され、GeneratorExit最後の の時点で例外がスローされ、yield例外によってコンテキスト マネージャーがファイルを閉じます。

CPython の参照カウントでは、これは通常すぐに発生します。ただし、非参照カウント実装または参照サイクルが存在する場合、ジェネレーターは、サイクル検出 GC パスが実行されるまで収集されない場合があります。これにはしばらく時間がかかる場合があります。


私の腸は、ファイルを閉じるのは呼び出し元に任せるように言っています。できるよ

for f in _iterdir('/', 'usr'):
    with f:
        make_unicorns_from(f)

withジェネレーターにa がなくても、例外がスローされても、それらはすべてすぐに閉じられます。これが実際にジェネレーターにファイルを閉じるよりも良い考えであるかどうかはわかりません。

于 2014-07-11T07:37:53.370 に答える
8

全体のポイントはwith、例外的な安全性と明示的なライフタイムで開閉を統一することです。あなたの抽象化はその一部を削除しますが、すべてではありません。

以下は、完全に単純化された例です。

def with_open():
    with open(...) as f:
        yield f

その使用法の例外を考えてみましょう:

for _ in with_open():
    raise NotImplementedError

これはループを終了しないため、ファイルは開いたままになります。おそらく永遠に。

不完全で、例外に基づく終了も考慮してください。

for _ in with_open():
    break

for _ in with_open():
    return

next(with_open())

1 つのオプションは、次のことができるように、コンテキスト マネージャー自体を返すことです。

def with_open():
    yield partial(open, ...)

for filecontext in with_open():
    with filecontext() as f:
        break

別のより直接的な解決策は、関数を次のように定義することです。

from contextlib import closing

def with_open(self, *path):
    def inner():
        for file in self._iterdir(*path):
            with file:
                yield file

    return closing(inner())

そしてそれを次のように使用します

with iterdir() as files:
    for file in files:
        ...

これにより、ファイルのオープンを呼び出し元に移動する必要なく、クロージャーが保証されます。

于 2014-07-11T15:19:47.227 に答える