LoginSignup
40
24

More than 5 years have passed since last update.

Pythonで「コールバック関数にクロージャーを渡す」

Posted at

はじめに

「コールバック関数にクロージャーを渡して・・」みたいな文章がすんなりと理解できない人(数日前の私)向けです。
コールバック関数に状態を渡したい。
そんな思いを実現するために、分かりにくいだろうと思われる文法要素をなるべく全部解説します。

この記事で扱っていること

  • コールバック関数(callback function)
  • 第一級関数(first-class function)
  • クロージャー(closure)

最初の目標

以下のようなコードが理解できることです。

シンプルな例
def apply_async(func, args, callback):
    result = func(*args)
    callback(result)

def add(x, y):
    return x + y

def print_result(result):
    print('Result:', result)

>>> apply_async(add, (2, 3), callback=print_result)
Result: 5

>>> apply_async(add, ('Hello', 'World'), callback=print_result)
Result: HelloWorld

このコードは結果だけを考慮すれば以下のコードと同じです。

実現したかったこと
>>> print('Result:', 2 + 3)
Result: 5

>>> print('Result:', 'HelloWorld')
Result: HelloWorld

当然、色んな応用が効くことにコールバック関数の存在価値があるのですが、本記事の目的はコールバック関数と呼ばれるものの文法的な意味が理解できることを目標とします。
どんな場面で使ったら便利なの?ということは範囲外とします。

シンプルな例

第1級関数

最初の関数 apply_asyncでは第1引数でfuncを指定しています。
関数の中で引数で渡した関数を使っています。
このように関数の引数として渡すことができるモノを第1級オブジェクト(first-class object)と言います。
Pythonではごく普通に関数をオブジェクトとして扱うことが出来るので、用語として第1級関数という言葉を使います。

可変長引数

第2引数のargsは関数の中で*argsとして参照しています。
このように書くことで引数を任意の数だけ渡すことができます。
シンプルな例では(2,3)とタプルで渡すことで、2つの引数を渡したことになります。
ここでは渡す先の関数funcが2つの引数のみを取りますが、別の関数に切り替えた時にも対応できるように、可変長引数を使っています。

result = func(*args)

第1引数で渡したfuncに第2引数で渡したargsを可変長引数として展開して、その実行結果をresultに格納します。
この部分は result = add(2, 3) として解釈されます。

callback(result)

第3引数としてcallback関数を渡します。
コールバック関数が何なのかは後に回すとして、ここではprint_result関数を渡しているので、 print_result(result)として解釈されます。

以上を組み合わせると実現したかったことが、無事に実現できたことになります。

コールバック関数とは何だったのか

別の関数にコールバック関数を引数で渡して、その関数の中でコールバック関数の処理を実行しました。
今回のコールバック関数の中身は単なるprint文です。
コールバック関数を切り替えることで、例えばprint文で一緒に表示する文を変更したり、全く別の処理をさせたりすることができます。
コールバック関数として渡してあげる関数を切り替えることで、全体の処理が大きく変化するのです。

出来ていないこと

シンプルな例ではコールバック関数に状態がありません。
状態が無いとは、例えば、コールバック関数が何回呼ばれたのかを確認しようとしてもその手段がありません。
例では2+3とHelloWorldを表示するために2回呼びましたが、この回数を表示させたいとしてもシンプルなコールバック関数では実現できません。
そこでクロージャーの出番です。

クロージャーを使った例

クロージャーの例
def make_handler():
    sequence = 0
    def handler(result):
        nonlocal sequence
        sequence += 1
        print('[{}] Result: {}'.format(sequence, result))
    return handler

handler = make_handler()


>>> apply_async(add, (2, 3), callback=handler)
[1] Result: 5

>>> apply_async(add, ('Hello', 'World'), callback=handler)
[2] Result: HelloWorld

クロージャーとは何か

関数に状態を渡したい、そんな時に出てくる用語がクロージャーです。
一つのメソッドしか持たないクラスと比較するのが分かりやすいと思われるので、その視点で解説します。

クラスとの比較

一つのメソッドしか持たないクラス
class ResultHandler:
    def __init__(self):
        self.sequence = 0
    def handler(self, result):
        self.sequence += 1
        print('[{}] Result: {}'.format(self.sequence, result))

このクラスはhandler関数しか持ちません。
やっていることは引数をprintするだけです。
self.sequenceに呼ばれた回数を記憶しておいて、実行するたびにそれを増加させています。
クラスの例としてはとてもシンプルなものだと思います。
これと同じ機能をもったモノを関数で書き直したもの、それがクロージャーです。

クロージャーで書き換えた
def make_handler():
    sequence = 0
    def handler(result):
        nonlocal sequence
        sequence += 1
        print('[{}] Result: {}'.format(sequence, result))
    return handler

形を見ると大分似ています。
気になるキーワードはnonlocalでしょうか。

nonlocalについて

nonlocalをちゃんと説明するとなると変数のスコープについて語る必要があります。
ここではごく簡単に説明します。
Pythonの関数では内部で定義した変数を関数の外で参照することは出来ません。
そのルールは関数の中で定義した関数に対しても影響していて、そのままでは読み取りをすることは出来ますが、書き込みすることは出来ません。

読み取りする例(OK)
def outer():
    hoge = 1
    def inner():
        print(hoge)
    return inner

>>> o = outer()
>>> o()
1

外側の関数outerで定義したhogeを内側の関数innerで正常に読み取ることが出来ています。
ちなみに関数を外れてprint(hoge)とするとエラーが出ます。

書き込みする例(NG)
def outer():
    hoge = 1
    def inner():
        hoge += 1
        print(hoge)
    return inner

>>> o = outer()
>>> o()
UnboundLocalError: local variable 'hoge' referenced before assignment

書き込みしようとした瞬間にそのhogeは内側の関数inner()でのみ通用するlocal変数として認識されるので、宣言する前の書き込みは駄目だとエラーが吐かれます。

nonlocalを使った例(大成功)
def outer():
    hoge = 1
    def inner():
        nonlocal hoge
        hoge += 1
        print(hoge)
    return inner

>>> o = outer()
>>> o()
2

以上から内側の関数から外側の関数を参照するにはnonlocalというキーワードが必要ということが分かりました。

自由変数

ところで、何故外側の関数で宣言した変数を内側の関数で参照したかったのでしょうか?
元々は関数に状態を持たせたいという話だったはずです。
関数の内部で扱う変数において、その場で消えてしまう泡沫な変数とは違って、ローカルスコープに束縛されない自由な変数が我々には必要です。
この自由変数を利用することで、関数に状態を持たせることができます。
自由変数は、クラスの例で言うフィールドに対応します。
フィールドはメソッドからアクセスすることができます。
自由変数もまた関数の内部で自由にアクセスすることができるので、同じように扱うことが出来るのです。
この自由変数を発生させるために行った、外側で宣言した変数を内側でnonlocalするという一連の処理を行った関数のことをクロージャーと呼びます。
すべては関数に状態を持たせることが目的です。

まとめ

以上で、状態を持ったコールバック関数を作ることが出来ました。
Pythonではnonlocalを使ったクロージャーをコールバック変数として渡してあげることで、呼び出し先で状態も一緒に参照させることができます。

これでもう、「コールバック関数にクロージャーを渡して・・」みたいな文章を見てもきっと大丈夫、なはずです。

参考文献

python-cookbook Code samples from the "Python Cookbook, 3rd Edition", published by O'Reilly & Associates, May, 2013.
→こちらのサンプルコードから例を引用させて頂きました。

40
24
0

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
40
24