Pythonのwith文とJava7のtry-with-resourceとC#のusingの比較

http://nhiro.org/learn_language/with_statement.html

Java7は名前の通りtryと抱き合わせになっている。C#Pythonは分離されている。なのでJava7でC#Pythonと同じ挙動をしたければ必要なくても空のfinallyを書くことになる。(追記: finallyやexceptを伴わないtryもOKでした。thanks id:nowokay)

本体が正常終了または例外を投げて異常終了した場合のどちらでも呼ばれる「後片付けメソッド」はC#だとDispose、Java7だとclose、
Pythonだと__exit__。しかしPython以外は引数を取らない。本体が正常に終了したのかどうかはどうやって知るんだろうか。知る必要はないという判断なんだろうか。追記: Pythonがどんな引数を取るのか他の言語の人には想像がつきにくいらしいので補足すると「例外の種類、例外のインスタンススタックトレース」の3つ組。要するに「例外が起きたかどうか、起きたならその詳しい情報」が渡されるということ。

具体的に例外が起きたかどうかで「後片付け」の処理を変えている例はPEP 343 -- The "with" Statementにはジェネレータに対して使っていて、例外終了した場合にはジェネレータの中に例外を投げ込んでみて、ジェネレータがその例外を捕捉して処理するチャンスを作っている。

Java7では例外はcatch節で捕まえる。つまり実装時に捕まえるかどうかを判断する。C#ではそもそも例外処理の構文ではないので例外は単純に貫通する。Pythonでは例外を貫通させるのか、__exit__で処理できたので貫通しないでいいのかは__exit__の返り値で決まる。

複数個のリソースを扱えることと、開いた順の逆順で閉じられることはどの言語でも共通。

Pythonだけ「ブロックに入る前に呼ばれるメソッド」を定義することができる。

追記: ささださんとの会話

(こういうチャットログをいい感じに読みやすくするのをサポートしてくれる汎用Togetterみたいなものがあったらいいのに。話がつながるように多少前後を入れ替えたが、それでも繋がっていないところがあるので括弧で囲った)

ko1: そもそも Python の引数が気になる。想像出来ない。> Pythonだと__exit__。しかしPython以外は引数を取らない。
nishio: 例外の型、例外のインスタンス、例外のスタックトレース、の3つセット
ko1: 例外の捕捉とリソースの解放は別なんじゃないの?
nishio: うーん、 失敗時と成功時でリソースの解放の仕方を変えたいケースとか?
ko1: そもそも失敗したらそのブロック実行しないというのが C# の usingじゃないのかなあ。
ko1: 例外によって後始末の方法を変えるのなら,catch(ruby だと rescue)で変えそうな。Ruby だと $! でエラー情報取れるから,それでやるってのも可能性はあるけど見たことはない
nishio: あー、それはリソース獲得時の失敗と本体の実行時の例外がごっちゃになってるな。今話していたのは、リソース獲得に成功した後、本体が正常終了したのか異常終了したのかによって__exit__の引数が異なるって話。
ko1: 「例外によって後始末の方法を変えるのなら…(catch, $!)」はそれを意図していた
nishio: うん、で「そもそも失敗したらそのブロック実行しない」はリソース獲得時の失敗のことだな、と。
ko1: それは獲得時の話ですね。で,それだと,そもそもブロック実行しないから,ブロックで例外が発生することもない
nishio: リソース獲得時に失敗したらブロックを実行しないってのは3つ全部の言語で同じだと思われ。
ko1: ブロックで例外発生してもしなくても,関係無くリソースは close したい気がする

(ko1:
begin
  a = open(...)
  b = open(...) # ここで失敗
ensure
  ここで何をするか
end
ko1: Ruby だとこういうので困ることが)


nishio: それも同じ>発生しててもしてなくても呼ばれる。で、論点はそのcloseの処理で例外が起きたかどうかを知る必要はないのかどうか。
ko1: 無いと思うなあ,というのがC# の気持ちだと思うし,俺もそういうシチュエーションが思いつかない。そういう経験あった?
nishio: 知る必要がないと断言できるほど詳しくない、が実情。
ko1: __exit__ の構文を知らないんだがそれでエラー内容を使ってるところを探してみるとか。
nishio: http://www.python.org/dev/peps/pep-0343/ ではジェネレータをラップするのに使ってるなぁ。ブロック内で例外が起きた時にまずはその例外をジェネレータの中に投げ込んでみて、ジェネレータの中のfinallyとかが働くチャンスを作るわけだな。
ko1: なんか難しいことをしているな。generator ってリソースなんだろうか... よくわからんが,Ruby だと try(rescue)しろよって気がする。ああ,だから Ruby には using 的なものはないのか。先日,ruby にも using 的なものあればいいよね,みたいな話をしてたんだけど自分でブロック作ってイテレータ書けよ,のほうが(open(){ ... } みたいなのを作れ,のほうが)柔軟なんだな
nishio: Ruby流の解決方法がいまいちピンとこない。
ko1: open(...){|f| ...} で block から解放されたら消えるじゃん。それは,open がブロック呼ぶ前に begin/ensure でくるんでるんだけどリソース解放は foo{...} の foo メソッドが責任を持つ,てきな空気がある。で,foo は,HogeException の時はなんかする,みたいなのが書ける。そういう意味で,__exit__ が例外を返す,というのと同じようなものなんだろうか,と思った。
nishio: まずopenというのは引数とブロックを取るメソッドなの?ファイルのopenかと思ってしまった。
ko1: ファイルのオープン。f = open(...) としてもいいけどopen(...){|f| ...} とも書ける
nishio: なるほど、ブロックを渡された時にはbegin/ensureでくるんで呼び出す、という設計になっているのか。
ko1: yes.設計にしている
nishio: ぶっちゃけwith文はブロック渡しのないPythonのためのブロック渡しなんじゃないのか。ブロックの前と後に実行する内容が__enter__と__exit__になってる。C#とかと違って、ブロックに入る前に実行する__enter__があるのもその辺。
ko1: なるほどね



ko1: 「try/finally statements」とあるのでリソースに特化した話じゃないのね

(ko1: loop{...} というメソッドは
ko1: def loop
  begin
    while true
      yield
    end
  rescue StopIteration
    # stop
  end
end
ko1: こうかいてあり,raise StopIteration とすると止まる
ko1: 他の raise Foo はそのまま突き抜ける)

ko1: この with 文は,Python ではリソース解放でもよく利用されている?
nishio: そもそも、「スコープを抜けるときに必ず実行したい処理」がリソースの解放のことが多いだけで、本質的に解決したかった問題は「例外の導入によって関数だのブロックだのから抜ける場所が予測できなくなったこと」だよね。それをfinallyが解決したわけだけども、その後「それを複数個に対してやりたいこともたくさんあるよね」というニーズに対して、try/finallyで書くとネストがどんどん深くなるからよい方法が欲しくなって、その解決がusingやtry-with-resourceやwith-statementという認識。
ko1: C# using はネスト深くならない?複数かけるのか,あれ
nishio: 複数書けるよ、っていうかサンプルコードには複数書いているよ、ほい http://nhiro.org/learn_language/with_statement.html
ko1:

        using (
            MyClass c1 = new MyClass("first"),
                    c2 = new MyClass("second"))
        {

ko1: これか
nishio: で、どの言語でも確保したのと逆順に開放する
ko1: これ,c1は作れて c2 失敗したときは ちゃんと c1 を解放すんのかね
nishio: 確認してないけどc1のdisposeが呼ばれるはず
ko1: まぁ,すんだろうなあ
nishio: 呼ばなかったら設計者を殴りに行った方がいいw


ko1: Java ってこんなこと書けるのか

try (
             AutoCloseable imp1 = new AutoCloseableImpl("first");
             AutoCloseable imp2 = new AutoCloseableImpl("second")) {

nishio: 7からね。
ko1: 同じように imp2 でこけても imp1 は close すると
nishio: 当然そうなっているだろうと思って確認してない。
ko1: ruby だと,open{|f1| open{|f2| ...}} って処理は結構書くから,ネスト深いな
ko1: with MyClass("first") as x, MyClass("second") as y: も,同じかなあ(y で失敗して,x で __exit__ する)


nishio: https://gist.github.com/3106388
nishio: xのinitが呼ばれて__enter__が呼ばれた後yの__init__で例外が飛んだ場合にbodyは実行されずにxの__exit__がエラー引数付きで呼ばれている。飛ばした例外が意図したものと違ったけどw
ko1: ひどい
nishio: JavaはRuntimeExceptionだから行き来していると混ざるw
ko1: ruby でもなんか考えて見るか