LoginSignup
2
1

More than 5 years have passed since last update.

finallyとyieldを組み合わせた関数の返し値(ジェネレータ)は直接nextに渡してはならない

Last updated at Posted at 2017-06-06

このようなコードがある。

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
2
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1