web-dev-qa-db-ja.com

Pythonで「with」と「yield」を組み合わせても安全ですか?

これは、python=の一般的なイディオムであり、ファイルを自動的に閉じるためにコンテキストマネージャを使用します。

with open('filename') as my_file:
    # do something with my_file

# my_file gets automatically closed after exiting 'with' block

今、私はいくつかのファイルの内容を読みたいです。データの利用者は、データがファイルからのものかファイル以外のものかを知りません。受け取ったオブジェクトが開いているかどうかを確認する必要はありません。それは何かを行から読み取るためのものです。だから私はこのようなイテレータを作成します:

def select_files():
    """Yields carefully selected and ready-to-read-from files"""
    file_names = [.......]
    for fname in file_names:
        with open(fname) as my_open_file:
            yield my_open_file

このイテレータは次のように使用できます:

for file_obj in select_files():
    for line in file_obj:
        # do something useful

(同じコードを使用して、開いているファイルではなく、文字列のリストを使用することもできます。これはすばらしいことです。)

質問です:開いているファイルを生成することは安全ですか?

「どうして?」のように見えます。コンシューマーがイテレーターを呼び出し、イテレーターがファイルを開き、コンシューマーに渡します。コンシューマーはファイルを処理し、次のイテレーターに戻ります。イテレータコードが再開し、「with」ブロックを終了します。my_open_fileオブジェクトが閉じられ、次のファイルに移動する、など。

しかし、消費者が次のファイルのイテレータに戻らない場合はどうでしょうか? F.e.コンシューマ内部で例外が発生しました。または、消費者はファイルの1つで非常にエキサイティングな何かを見つけ、喜んでそれを誰にでも呼んだ結果を返しましたか?

この場合、イテレータコードは再開されず、「with」ブロックの最後に到達することはなく、my_open_fileオブジェクトは決して閉じられません!

それとも?

31
lesnik

あなたは以前に提起された批判を持ち出します1。この場合のクリーンアップは非決定的ですが、 will CPythonで発生し、ジェネレーターがガベージコレクションを取得します。 マイレージは他のpython実装...

ここに簡単な例があります:

_from __future__ import print_function
import contextlib

@contextlib.contextmanager
def manager():
    """Easiest way to get a custom context manager..."""
    try:
        print('Entered')
        yield
    finally:
        print('Closed')


def gen():
    """Just a generator with a context manager inside.

    When the context is entered, we'll see "Entered" on the console
    and when exited, we'll see "Closed" on the console.
    """
    man = manager()
    with man:
        for i in range(10):
            yield i


# Test what happens when we consume a generator.
list(gen())

def fn():
    g = gen()
    next(g)
    # g.close()

# Test what happens when the generator gets garbage collected inside
# a function
print('Start of Function')
fn()
print('End of Function')

# Test what happens when a generator gets garbage collected outside
# a function.  IIRC, this isn't _guaranteed_ to happen in all cases.
g = gen()
next(g)
# g.close()
print('EOF')
_

このスクリプトをCPythonで実行すると、次のようになります。

_$ python ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
EOF
Closed
_

基本的に、使い果たされたジェネレーターの場合、コンテキストマネージャーは期待どおりにクリーンアップすることがわかります。ジェネレータがを使い果たしていない場合、クリーンアップ機能は、ジェネレータがガベージコレクタによって収集されたときに実行されます。これは、ジェネレーターがスコープから外れた場合(または遅くとも次の_gc.collect_サイクルでのIIRC)に発生します。

ただし、いくつかの簡単な実験(たとえば、上記のコードをpypyで実行する)を実行すると、コンテキストマネージャーのすべてがクリーンアップされません。

_$ pypy --version
Python 2.7.10 (f3ad1e1e1d62, Aug 28 2015, 09:36:42)
[PyPy 2.6.1 with GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)]
$ pypy ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
End of Function
Entered
EOF
_

したがって、コンテキストマネージャーの___exit___ will がすべてのpython=実装に対して呼び出される)との主張は正しくありません。ここでのミスは、 pypyのガベージコレクション戦略 (これはではない参照カウント)であり、pypyがジェネレータを取得することを決定するまでに、プロセスはすでにシャットダウンするので、気になりません...ほとんどの実際のアプリケーションでは、ジェネレーターはおそらく取得され、実際には問題にならないほど迅速にファイナライズされます...


厳格な保証の提供

コンテキストマネージャーが適切にファイナライズされることを保証したい場合は、 close ジェネレーターを使い終わったらジェネレーターに注意してください2。上記のg.close()行のコメントを外すと、GeneratorExityieldステートメント(コンテキストマネージャー内にあります)で発生し、それによってキャッチ/抑制されるため、確定的なクリーンアップが得られます。発生器...

_$ pypy ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF

$ python3 ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF

$ python ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF
_

FWIW、これは_contextlib.closing_を使用してジェネレータをクリーンアップできることを意味します:

_from contextlib import closing
with closing(gen_function()) as items:
    for item in items:
        pass # Do something useful!
_

1最近では、イテレータのクリーンアップをより確定的にすることを目的とした PEP 5 を中心に議論が行われています。
2すでに閉じられているか消費されているジェネレーターを閉じても、ジェネレーターの状態を気にすることなく呼び出すことができます。

16
mgilson

Pythonで「with」と「yield」を組み合わせても安全ですか?

私はあなたがこれをするべきではないと思います。

いくつかのファイルの作成を示します。

>>> for f in 'abc':
...     with open(f, 'w') as _: pass

ファイルが存在することを納得してください。

>>> for f in 'abc': 
...     with open(f) as _: pass 

そして、ここにあなたのコードを再作成する関数があります:

def gen_abc():
    for f in 'abc':
        with open(f) as file:
            yield file

ここでは、関数を使用できるように見えます:

>>> [f.closed for f in gen_abc()]
[False, False, False]

しかし、最初にすべてのファイルオブジェクトのリスト内包を作成しましょう。

>>> l = [f for f in gen_abc()]
>>> l
[<_io.TextIOWrapper name='a' mode='r' encoding='cp1252'>, <_io.TextIOWrapper name='b' mode='r' encoding='cp1252'>, <_io.TextIOWrapper name='c' mode='r' encoding='cp1252'>]

そして今、それらはすべて閉じられていることがわかります:

>>> c = [f.closed for f in l]
>>> c
[True, True, True]

これは、ジェネレータが閉じるまで機能します。その後、ファイルはすべて閉じられます。

遅延評価を使用している場合でも、最後のファイルはおそらくそれを使用する前に閉じられるでしょう。

7
Aaron Hall