Python の「with」ブロック内から yield するのは安全ですか (その理由も)?
質問
コルーチンとリソース取得を組み合わせると、意図しない (または直感的ではない) 結果が生じる可能性があるようです。
基本的な問題は、次のようなものが機能するかどうかです。
def coroutine():
with open(path, 'r') as fh:
for line in fh:
yield line
それはそうなります。(テストしてみることもできますよ!)
さらに深い懸念は、 with
の代替となるものであるはずです finally
, 、ブロックの最後にリソースが解放されるようにします。コルーチンは実行を一時停止および再開できます。 内で の with
ブロックしますので、 紛争はどのように解決されますか?
たとえば、コルーチンがまだ返されていないときに、コルーチンの内部と外部の両方でファイルを読み取り/書き込みで開くと、次のようになります。
def coroutine():
with open('test.txt', 'rw+') as fh:
for line in fh:
yield line
a = coroutine()
assert a.next() # Open the filehandle inside the coroutine first.
with open('test.txt', 'rw+') as fh: # Then open it outside.
for line in fh:
print 'Outside coroutine: %r' % repr(line)
assert a.next() # Can we still use it?
アップデート
前の例では書き込みロックされたファイル ハンドルの競合を想定していましたが、ほとんどの OS はプロセスごとにファイル ハンドルを割り当てるため、競合は発生しません。(例があまり意味をなさないことを指摘してくれた @Miles に敬意を表します。) これが実際のデッドロック状態を示す、私が修正した例です。
import threading
lock = threading.Lock()
def coroutine():
with lock:
yield 'spam'
yield 'eggs'
generator = coroutine()
assert generator.next()
with lock: # Deadlock!
print 'Outside the coroutine got the lock'
assert generator.next()
解決
私は本当にあなたがについて尋ねているのか葛藤を理解し、また例の問題点はありません:それは、同じファイルへの2つの共存、独立したハンドルを持って大丈夫です。
。一つのことは私が発電機に新しいclose()メソッドがあることをあなたの質問、それに応じて、学んだことを知りませんでした。
close()
は、反復を終了するために発電機内部の新しいGeneratorExit
例外を発生させます。この例外を受信すると、発電機のコードのいずれかGeneratorExit
またはStopIteration
を上げる必要があります。
close()
は発電機がガベージコレクトされたときに呼び出されるので、これは発電機のコードは、発電機が破壊される前に実行する最後のチャンスを得る手段です。この最後のチャンスは、発電機でtry...finally
の文は、現在の動作が保証できることを意味します。finally
句は現在、常に実行する機会を得るでしょう。これは、言語トリビアのマイナービットのように思えるが、使用して発電機やtry...finally
はPEP 343で記述with
ステートメントを実行するために実際に必要である。
それはwith
文を発電機に使用されている状況を処理し、それが途中で生じませんが、決して返し、発電機がガベージコレクトされるとき、コンテキストマネージャの__exit__
メソッドが呼び出されますので。
編集
ファイルハンドルの問題に関して:私は時々、POSIXに似ていないプラットフォームが存在することを忘れています。 :)
限りロックが行くように、私は彼が言うときラファウDowgird爪の上に頭を打つと思う「あなただけの発電機だけでリソースを保持している他のオブジェクトと同様であることを認識する必要があります。」この機能は、同じデッドロックの問題に苦しんでいるので、私は、with
文はここで本当に関連しているとは思わない:
def coroutine():
lock.acquire()
yield 'spam'
yield 'eggs'
lock.release()
generator = coroutine()
generator.next()
lock.acquire() # whoops!
他のヒント
私は本当の競合があるとは思いません。あなただけの発電機がちょうどリソースを保持している他のオブジェクトと同様であることを認識する必要があり、それが適切に確定されていることを確認するために(およびオブジェクトが保持しているリソースとの競合/デッドロックを回避するために)作成者の責任です。私はここを参照してくださいのみ(マイナー)の問題は、発電機は、(少なくともPythonの2.5のような)コンテキスト管理プロトコルを実装していないということですので、あなただけではなくできます:
with coroutine() as cr:
doSomething(cr)
代わりに持っています:
cr = coroutine()
try:
doSomething(cr)
finally:
cr.close()
ガベージコレクタはとにかくclose()
を行いますが、それはリソースを解放するためにそれに頼るのは悪い習慣です。
yield
が任意のコードを実行することができますので、私はyield文の上にロックを保持しているのは非常に警戒すると思います。あなたは上書きあるいは変更されているかもしれないメソッドや関数を呼び出すなどの、しかし、他の多くの方法で同様の効果を得ることができます。
ジェネレータ、しかし、常に(ほぼ常に)「閉」、明示的なclose()
呼び出しで、または単にガベージコレクションであることによってです。発電機を閉じると、発電機内部のGeneratorExit
例外がスローされますので、finally節を実行し、などの文のクリーンアップ、とあなたは、例外をキャッチすることができますが、むしろ降伏よりも、(すなわちStopIteration
例外をスロー)機能を投げるか、終了する必要があります。それはあなたが書いたようにあなたが望むかもしれないよりそれが後に起こる可能性があるため、場合によっては発電機を閉じるためにガベージコレクタに依存するため、おそらく貧しい練習だし、誰かがsys._exit()を呼び出した場合、その後、あなたのクリーンアップがまったく起こらない可能性がありますます。
それは私が仕事に物事を期待どのようになります。はい、それが完了するまでブロックは、そのリソースを解放しないので、その意味では、リソースは、それが字句営巣だ脱出しました。しかし、これはブロックと内の同じリソースを使用しようとした関数呼び出しを行うことに違いはありません - 何もブロックが、のないのまだのどんな<ために、終了した場合には、あなたを助けていません/ em>の理由。それは実際に発電機に固有のものではありません。
行動ですが、気にせ価値があるかもしれないことの一つは、発電機がある場合は、のことはありませんのを再開しました。私はwith
ブロックのように動作し、終了時にfinally
の一部を呼び出すために__exit__
ブロックを期待しているだろうが、それはケースではないようです。
TLDR の場合は、次のように見てください。
with Context():
yield 1
pass # explicitly do nothing *after* yield
# exit context after explicitly doing nothing
の Context
後に終了 pass
完了しました(つまり、何もない)、 pass
後に実行される yield
完了しました(つまり、実行が再開されます)。それで、 with
終わる 後 制御が再開されるのは yield
.
TLDR:あ with
コンテキストは次の場合に保持されたままになります yield
コントロールを解放します。
実際、ここで関連するルールは 2 つだけです。
いつ
with
リソースを解放しますか?そうなります 一度 そして直接的に 後 そのブロックは完了しました。前者はリリースしないことを意味します その間 ある
yield
, 、それは何度か起こる可能性があるためです。後者はリリースすることを意味します 後yield
完了しました。いつ
yield
完了?のことを考える
yield
逆呼び出しとして:制御は呼び出された側ではなく、呼び出し側に渡されます。同様に、yield
呼び出しが制御を返すときと同様に、制御が返されると完了します。
どちらも注意してください with
そして yield
ここでは意図したとおりに機能しています!のポイント with lock
リソースを保護することであり、リソースは保護され続けます。 yield
. 。この保護はいつでも明示的に解除できます。
def safe_generator():
while True:
with lock():
# keep lock for critical operation
result = protected_operation()
# release lock before releasing control
yield result