このようなコードがある。
def yielder():
#try:
print('yielder setup')
yield 'text'
#finally:
print('yielder teardown')
def yielder_with_finally():
try:
print('yielder setup')
yield 'text'
finally:
print('yielder teardown')
def f(text):
print(text)
yielderを呼び出すとき、for y in yielder(): f(y)
とすると、出力は
yielder setup
text
yielder teardown
となるが、nextを1回しか呼び出さないような使い方(※後ろに書かれた4つの方法のうち前者を除いた3つ)をすると、
yielder setup
text
のようになる。teardownは呼ばれていない。finallyを付けることでteardownが呼ばれるようになる。
しかし、この関数を呼び出すとき、for y in yielder_with_finally(): f(y)
やy=yielder_with_finally();f(next(y))
では
yielder setup
text
yielder teardown
という予期した出力になるが、t=next(yielder_with_finally());f(t)
やf(next(yielder_with_finally()))
という呼び出し方をすると、
yielder setup
yielder teardown
text
という予期せぬ実行順序になってしまう。CPython2/3で同様の挙動であった。
ただ、nextを1回呼んだだけでteardownが実行されると(IPython等での)デバッグが困難になるので、そもそもこういう使い方はしてはいけないのかもしれない。 yieldする場合のteardownはfinallyを使わないのが得策かも。
なおpytestのyield fixtureではちゃんと2回回す実装になっているので、この不具合にはかからない。
https://docs.pytest.org/en/latest/_modules/_pytest/fixtures.html
PyPyではfinallyの有無にかかわらず、CPythonでfinallyがない場合と同じ挙動であった。
Rubyでも検証したが、ensureの有無で挙動は変わらなかった(PyPyと同様)。
def yielder()
return to_enum(:yielder) if !block_given?
#begin
puts('yielder setup')
yield 'text'
#ensure
puts('yielder teardown')
#end
end
def yielder_with_ensure()
return to_enum(:yielder_with_ensure) if !block_given?
begin
puts('yielder setup')
yield 'text'
ensure
puts('yielder teardown')
end
end
def f(text)
puts(text)
end