Python – クロージャ (Closure)
本記事ではPythonのクロージャ(closure)を整理します。 Python – ファーストクラス関数 (First Class Function) を先に読んでおくと理解がスムーズです。
クロージャとは何でしょうか。日本語版Wikipediaに適切な説明が見当たらなかったため、英語版Wikipediaの定義を参照します。
In programming languages, closures (also lexical closures or function closures) are techniques for implementing lexically scoped name binding in languages with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment:[1] a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.[b] A closure—unlike a plain function—allows the function to access those captured variables through the closure’s copies of their values or references, even when the function is invoked outside their scope.
訳すと次のようになります。
プログラミング言語におけるクロージャとは、ファーストクラス関数をサポートする言語における名前束縛(name binding)の手法です。クロージャは、ある関数を、その関数自身が持つ環境とともに保存したレコードです。さらに、関数が持つ自由変数(free variable)を、クロージャが作られた時点での値や参照に対応づける役割も担う。クロージャは通常の関数とは異なり、自分のスコープ外で呼び出された関数の変数値や参照をコピーして保存しておき、このキャプチャした値にアクセスできるようにしてくれる。
自由変数(free variable)とは何でしょうか。
Pythonにおける自由変数とは、コードブロックの中で使用されているけれども、そのコードブロックの中で定義されていない変数を指します。 自由変数については、別の記事で詳しく説明します。
定義だけでは理解しにくいので、サンプルコードを通して見ていきます。
お好きなディレクトリにclosure.pyという名前のファイルを作り、次のコードを保存してください。
def outer_func(): # 1
message = 'Hi' # 3
def inner_func(): # 4
print message # 6
return inner_func() # 5
outer_func() # 2保存したら、ターミナルやコマンドプロンプトを開いて、ファイルを保存したディレクトリに移動し、次のコマンドでプログラムを実行してみましょう。
$ python closure.py
Hiプログラムを実行すると「Hi」という文字が出力されました。簡単な構文ですが、クロージャを説明するために「Hi」が出力されるまでのプロセスを一つずつ確認してみましょう。
#1で定義された関数outer_funcを#2で呼び出します。もちろん、outer_funcは何の引数も受け取りません。 outer_funcが実行された後、最初に行うのは、messageという変数に’Hi’という文字列を代入することです。#3です。 #4でinner_funcを定義し、#5でinner_funcを呼び出すと同時にreturnしています。 #6でmessage変数を参照して出力します。ここでmessageはinner_funcの中で定義されていませんが、inner_funcの中で使われているので自由変数と呼びます。 「Hi」が出力されるまでの一連のプロセスは以上のとおりです。ここまでは理解しにくい点は何もありません。それでは次のステップに進んでみましょう。
#5のコードを少し修正して実行してみます。
def outer_func(): # 1
message = 'Hi' # 3
def inner_func(): # 4
print(message) # 6
return inner_func # 5 <-- () を消しました。
outer_func() # 2$ python closure.py何も出力されないことが分かります。#5でouter_funcがreturnする際にinner_func関数を実行せず、関数オブジェクトをreturnしたためです。
それでは今度は、outer_funcがreturnするinner_funcオブジェクトを変数に代入してみます。
#2のコードを修正して実行してみます。
def outer_func(): # 1
message = 'Hi' # 3
def inner_func(): # 4
print(message) # 6
return inner_func # 5
my_func = outer_func() # 2 <-- 戻り値の inner_func を変数に代入します。$ python closure.pyもちろん今回も出力される値はありません。
今度は、my_func変数に本当にinner_func関数が代入されているか確認します。#7行を追加して実行してみましょう。
def outer_func(): # 1
message = 'Hi' # 3
def inner_func(): # 4
print(message) # 6
return inner_func # 5
my_func = outer_func() # 2
print(my_func) # 7 <-- 追加$ python closure.py
<function outer_func.<locals>.inner_func at 0x000001DCADB2AA60>本当に inner_func 関数が代入されていますね。それでは my_func 変数を使って inner_func 関数を呼び出してみましょう。
def outer_func(): # 1
message = 'Hi' # 3
def inner_func(): # 4
print(message) # 6
return inner_func # 5
my_func = outer_func() # 2
my_func() # 7
my_func() # 8
my_func() # 9$ python closure.py
Hi
Hi
Himy_func() を3回実行して「Hi」を3回出力しました。ところが、何かおかしいですね。outer_func は確かに#2で呼び出された後、終了しました。しかし、#7、#8、#9で呼び出された my_func (※ここで my_func は inner_func と同じであることを覚えておいてください。)関数が outer_func 関数のローカル変数である message を参照したのです。うーん…不思議ですね…どうやって可能だったのでしょうか。その答えがクロージャです。クロージャは関数の自由変数の値をどこかに保存していると言ったような気がします。一体どこに保存しているのでしょうか。一緒にその秘密を解き明かしてみましょう。笑
まずは、コードを次のように修正してから実行してみましょう。
def outer_func(): # 1
message = 'Hi' # 3
def inner_func(): # 4
print(message) # 6
return inner_func # 5
my_func = outer_func() # 2
print(my_func) # 7
print()
print(dir(my_func)) # 8
print()
print(type(my_func.__closure__)) # 9
print()
print(my_func.__closure__) # 10
print()
print(my_func.__closure__[0]) # 11
print()
print(dir(my_func.__closure__[0])) # 12
print()
print(my_func.__closure__[0].cell_contents) # 13$ python closure.py
\<function outer_func.\<locals\>.inner_func at 0x000002913914AA60\>
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__ha
sh__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subcl
asshook__']
<class 'tuple'>
(<cell at 0x000002913910B730: str object at 0x0000029139147B70>,)
<cell at 0x000002913910B730: str object at 0x0000029139147B70>
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
Hi出力された結果を一つずつ確認していきます。
#7の結果:my_func 変数に inner_func 関数オブジェクトが代入されていることは、上でも確認しました。
#8の結果:クロージャが一体どこにデータを隠しているのか、dir() コマンドを使って my_func の名前空間を確認してみました。なるほど!__closure__ という属性が隠れていることが分かりました。
#9の結果:__closure__ がどんな型なのか確認してみました。型はタプルだそうです。
#10の結果:そのタプルの中に何が入っているのか確認してみました。アイテムが一つ入っていますね。
#11の結果:タプルの中の最初のアイテムです。cell という文字列のオブジェクトですね。
#12の結果:この cell オブジェクトはどんな属性を持っているのでしょうか。ここに何か答えがありそうです。「cell_contents」という属性がありますね。
#13の結果:cell_contents に入っている値を確認します。‘Hi’が入っています。
これでクロージャがデータを保存している場所を確認できました。
ここから関数のパラメータを使うと、もっと面白いコードを書くことができます。次のコードを保存して実行してみましょう。
def outer_func(tag): # 1
text = 'Some text' # 5
tag = tag #6
def inner_func(): # 7
print('<{0}>{1}<{0}>'.format(tag, text)) # 9
return inner_func # 8
h1_func = outer_func('h1') # 2
p_func = outer_func('p') # 3
h1_func() # 4
p_func() # 10$ python closure.py
<h1>Some text<h1>
<p>Some text<p>outer_func 関数一つで、h1タグとpタグで文字列を囲む関数を作ってみました。今度は、タグの中の文字列をコントロールできる関数を作ってみましょう。
def outer_func(tag): # 1
tag = tag # 5
def inner_func(txt): # 6
text = txt # 8
print('<{0}>{1}<{0}>'.format(tag, text)) # 9
return inner_func # 7
h1_func = outer_func('h1') # 2
p_func = outer_func('p') # 3
h1_func('h1 タグの中身です。') # 4
p_func('p タグの中身です。') # 10$ python closure.py
<h1>h1 タグの中身です。<h1>
<p>p タグの中身です。<p>クロージャは、このように一つの関数で複数の関数を簡単に作り出せるようにしてくれますし、既に作られている関数やモジュールなどを修正せずに、wrapper関数を使ってカスタマイズできるようにしてくれる、なかなか優秀な仕組みです。
次回はクロージャを活用した代表的なパターンであるデコレータを取り上げます。