はじめに
あるライブラリがあり、コールバックを渡すのだがライブラリが本当に渡したコールバック関数を呼んだか調べたいとします。
そんなケースあるの?と思われるかもしれませんが実際にあります。これから説明することの元ネタはWSGIのPEPに載っていたサンプルコードです。
WSGIそのもので説明するとややこしくなるので単純化した以下のapplication関数で説明します。
def application(callback):
callback(123)
うまくいかない方法
とりあえず以下のように書いてみます。
def process_bad(app):
data = None
def callback(arg):
data = arg
app(callback)
print(data)
見出しでネタバレしていますがこの書き方ではうまくいきません。
>>> process_bad(application)
None
何故Noneかというと、Pythonではcallbackにてdataへの代入が行われているので、callback内にdataというローカル変数を作成するためです。このdataはprocess_badのdataと無関係なため、process_badのdataは初めに代入したNoneのままということになります。
ちなみに、代入せずに使うだけならprocess_badのdataが使われます(伏線)
それnonlocalでできるよ?
Python3の場合、nonlocalを使うと事は解決します。
def process_nonlocal(app):
data = None
def callback(arg):
nonlocal data
data = arg
app(callback)
print(data)
>>> process_nonlocal(application)
123
nonlocalは、
nonlocal 文は、列挙された識別子がグローバルを除く一つ外側のスコープで先に束縛された変数を参照するようにします。
というわけで外側のdataが参照(代入した場合、そちらに代入する)ようになります。
ただしPython2.7ではこの方法は使えません。
代入じゃないからローカル変数定義されないもん
先ほどちらっと書いた
ちなみに、代入せずに使うだけならprocess_badのdataが使われます
ということを悪用した方法。
def process_bad_knowhow(app):
data = {}
def callback(arg):
data['arg'] = arg
app(callback)
print(data['arg'])
呼んでみるとちゃんとKeyErrorにもならずに渡された引数が表示されます。
>>> process_bad_knowhow(application)
123
何が起きているのか。
callback関数内で行われているのは「dataが参照している辞書オブジェクトへの代入」です。「dataへの代入」ではありません。この場合、callbackで新たにローカル変数dataは定義されず、process_bad_knowhowのdataが参照されることになります。
というテクニックをWSGIのPEPで見かけて「うはぁ」と思ったわけですが、非常にバッドノウハウな感じがしますね(個人の感想です)
__call__使おうぜ
もう少しかっこいい方法を、ということで、相手方は渡されたものを呼べればいいわけなのでこんな方法を考えました。
def process_inner_class(app):
class Callback:
def __call__(self, arg):
self.arg = arg
callback = Callback()
app(callback)
print(callback.arg)
クラスに__call__メソッドを定義しておくと、オブジェクトが関数的に呼び出せるようになります。これを利用すれば相手方の「コールバック関数渡せ」という要求にも応えることができます。メソッドなので自分の属性に引数を保存しておくのも問題なくできます。
もう1つ説明。Pythonでは上記のように関数の中にクラス定義を書くことができます。関数に渡された引数を使ってクラス定義を変えることもできるし、もちろん戻り値として返すこともできます。まあ普通のプログラムを書く際には使わないと思いますが。Djangoでは使われていました。
コールバックとしてメソッドを渡す
さて最後の方法。ここまで呼ぶ側は関数でしたがクラスを使うと以下のように書けます。
class ProcessClass:
def process(self, app):
app(self.callback)
print(self.arg)
def callback(self, arg):
self.arg = arg
process = ProcessClass()
process.process(application)
ポイントは、「self.callback」これにより相手方(application関数)には「callbackメソッド」が渡され、相手方が渡されたものを呼び出すと「callbackメソッド」が実行されます。インスタンスに関連付いているので属性に引数を保存することができます。
「callback呼ぶときって引数1つじゃないの?なんで例外にならないで呼べるの?」という疑問に対しての回答。application関数には「メソッドオブジェクト」が渡されます。この「メソッドオブジェクト」を呼び出すと「メソッドオブジェクトに関連付いたインスタンスが引数の先頭に付け加えられて」呼び出されます。つまり、
- 呼んでる方は1引数で呼んでるつもり
- Pythonがこっそりインスタンス参照をprepend
- callbackメソッドが呼び出される
ということになります。
まとめ
コールバック関数が呼ばれたことを覚えていく方法として次の4つの手法を紹介しました。
- Python3ならnonlocalが一番簡単
- 新たなローカル変数が作られないように辞書オブジェクトを使う
- クラスに__call__メソッドを定義すると関数的に呼び出せます(コールバックの条件を満たします)
- そもそもクラスにしてメソッド渡すことも可能です