前の関連記事:Python(5)クロージャ(Closure)のお勉強
Python(3)WSGI:ウェブページからshutdown()メソッドでWSGIサーバを停止させるで作ったマルチスレッド化したWSGIサーバの例をいじって、高階関数、関数として使えるインタンス、ジェネレータを使ってみました。
グローバル変数は関数内で一度でも代入するとローカル変数と解釈される
マルチスレッド化したWSGIサーバの例これに手を加えていきます。
URLに代わってクロージャに値を持たす例
URLに値を持たせることをやめて関数のスコープに値を持たせました(クロージャ)。
値をリセットする方法に頭を悩ませました。
結局25行目と26行目でやっているようにglobal文を使うことで解決しました。
関数の中で参照のみされる変数は暗黙のうちにグローバルと解釈されて、関数の中で1度でも代入される変数は暗黙のうちにローカルと解釈されてしまいます。
(Python のローカルとグローバル変数のルールは何ですか?)
変数calcとcountは42行目と43行目でグローバルスコープで代入しているのでグローバル変数になります。
それを18行目の関数appのなかで参照しています。
参照しているだけならなにも問題は起きないのですが、これらに27行目と28行目でやっているように新たに値を代入しようとするとそれはローカル変数と解釈されてエラーになってしまいます。
そこで25行目と26行目でこれらの変数はグローバル変数ですよと宣言することでエラーがでなくなります。(なぜ変数に値があるのに UnboundLocalError が出るのですか?)
ちなみに関数appの一つ外のスコープはグローバルなのでPython(5)クロージャのお勉強ででてきたnonlocal文が使えるかと思いましたがダメでした。
nonlocal文はローカルスコープが入れ子になった状態で使うもののようです。
グローバル変数を使えば高階関数を使わなくても同じことができる
高階関数の引数をリストにした例
引数をリストにして関数calculator()と関数counter()を一つにまとめました。
リストは参照渡しになるのでnonlocal文が不要になります。
19行目でglobal文が必要なことには変わりはありません。
関数calc()を呼び出すたびに値が更新されてしまうので21行目でやっているように戻り値は1回で受け取らないといけません。
別に6行目にあるように高階関数を使わなくてもグローバル変数を使えば同じことができます。
グローバルスコープに値を持たす例
こちらのコードの方が短くてわかりやすいのですが、グローバルスコープの変数を使っているので値の書き換えに注意しないといけません。
高階関数を使った場合は高階関数のローカルスコープの変数になるので変数自体の扱いに気を使わなくて済むのですが、高階関数の戻り値の関数がグローバルスコープになっています。
WSGIアプリをクラスで書く
WSGIアプリをクラスにした例
これは32行目でグローバルスコープでクラスAppClassをインスタンス化してそのインスタンスを34行目で関数app()として呼び出しています。
メソッド__call__()はインスタンスが関数として呼び出されてたときにその引数も渡されて呼び出される特殊メソッドです。(object.__call__(self[, args...]))
この例ではインスタンス変数self.calcに値を持たせています。
次に32行目でインスタンス化するときに使って初期値を16行目でリセットするときに使おうとして初期値をメンバー変数に保存してみました。
インスタンス化するときの初期値を保存する例
self.calc = self.lst.copy() # self.lstからself.calcを取得。ここでcopy()を使わずself.calc = self.lstだとリストは参照渡しになるのでself.calcを更新するとself.lstも同時に更新されてしまいます。
高階関数で初期値を保持する
高階関数に初期値を保持させた例
高階関数に保持というか6行目の高階関数calculator()のローカルスコープを保ったままその中の関数f()を実行させています。(このローカルスコープを保った関数f()とそのローカルスコープとのペアをクロージャという、、、と思う。)
14行目で関数f()のローカルスコープでその外側のスコープのリストlstに代入しているので9行目でnonlocal文がないとエラーになります。
11行目と12行目のリストlstの各要素への代入のときはエラーにならなかったのにリスト全部の置き換えではnonlocal文がないといけないようです。
ジェネレータで値を保持する
ジェネレータで値を保持する例
関数でreturnで値を返すとその関数のローカルスコープは消滅します。
ところがreturnではなくyieldで値を返すとローカルスコープは消滅せずに保持されます。
このローカルスコープとyieldで返す関数がクロージャということになるでしょう。
yieldで値を返すこの関数をとくにジェネレータといいます。(ジェネレータ (generator))
ジェネレータには最初に呼び出したときの引数以外に途中で値を渡すことができます。(ジェネレータに値を渡す)
そこで22行目でジェネレータに値を渡すsend()メソッドを使ってリセットしています。
send()メソッドで渡された値をジェネレータで受け取る方法がなかなか理解できなくて苦労しました。
値を返すyield文そのものがsend()メソッドの引数を返すのですよ。
ということでsend()メソッドの引数で場合分けしようと思うとyield文を最初にもってこないといけません。
なので8行目でwhile True:を使って永久ループにして最後に来るはずのyield文を最初にもってきてsend()メソッドの引数を受け取っています。
計算する前に値を返しているので最初の出力値が[2, 4]ではなくて[1, 2]から始まっています。
これまでの例と同様に最初の戻り値も計算して返す例
WSGIサーバはシャットダウンしたあと1回だけ戻り値を受け取る
WSGIアプリの関数app()は戻り値をリストでreturnしていますがyieldで返せばリストで返さなくてもよいです。
WSGIアプリの戻り値をyieldで返す例
この例では面倒になっているだけですが、1行ずつばらばらに処理して逐一出力したいときにはyieldを使えばよさそうですね。
20行目はreturnのままでよさそうに思いましたがやってみるとWSGIサーバには値が返らず終了します。
yieldにすると「終了」にするとブラウザに「終了」とちゃんと表示されますがPythonプロセスはエラーを出して終了します。
WSGIアプリの戻り値をyieldで返す例 修正版
WSGIサーバをシャットダウンしたあとに返すyield文を一つだけにしたらエラーがでなくなりました。
WSGIサーバをシャットダウンしたあとは1回だけ戻り値を受け取るようです。
ほんとうは何がしたかったの?
本当はグローバルスコープで最初に関数を取得したりインスタンス化せずにappのローカルスコープだけでなんとかしたかったのです。
でもいずれの方法も結局if __name__ == "__main__": に続くグローバルスコープの行でなにかしないといけませんでした。
ローカルスコープでグローバル変数を定義する例
24行目でローカルスコープでグローバル変数calcを宣言しています。
まあせいぜいこれぐらいしか思いつきませんでした。
参考にしたサイト
global文
ローカルスコープでグローバル変数に代入するのに必要です。
Python のローカルとグローバル変数のルールは何ですか?
代入するかしないかで解釈が変わります。
なぜ変数に値があるのに UnboundLocalError が出るのですか?
異なるスコープで変数に代入するときはglobal文やnonlocal文が必要になります。
nonlocal文
高階関数のローカルスコープの変数に引数関数のスコープで代入するときに必要です。
object.__call__(self[, args...])
インスタンスが関数として “呼ばれた” 際に呼び出されます。
ジェネレータ (generator)
関数でyield文で値を返すとローカルスコープの状態が保持されます。
ジェネレータに値を渡す
ジェネレータのsend()メソッドはyield文の値で受け取ります。
0 件のコメント:
コメントを投稿