はじめに
Pythonのクロージャを勉強していて、
def outer():
x = 0
def inner():
print(x)
return inner
func = outer()
func() # -> 0
func() # -> 0
というコードを見たとき、func()を呼び出した時になぜ変数xを用いることができるのか疑問に思ったので調べてみました。
Pythonでのメモリ管理
Pythonでは、参照カウンタという方法を用いてメモリ管理を行っています。
それぞれのオブジェクトは、いくつの変数から参照されているかという情報を持っていて、その値が0になる(=どの変数からも参照されていない)といずれ破棄されます。
ということは、関数outer()の実行が終了しても変数xがfunc()内で使えるということは、func()内では何らかの方法でxを参照していると考えられます。そこで、どのように参照しているかを調べていきます。
関数オブジェクト
Pythonでは関数もオブジェクトであり、複数の属性を持っています。属性の種類については以下の通りになります。
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
この中で、__closure__属性の意味を見てみます。Python公式ドキュメントからの引用を示します。
None または関数の個々の自由変数 (引数以外の変数) に対して値を束縛しているセル (cell) 群からなるタプルになります。
よってセルを通して外側の関数中のオブジェクトは参照されるので、外側の関数の実行が終了してもオブジェクトは破棄されず、そのオブジェクトをクロージャから使うことができるといえそうです。
プログラムを実行して確かめてみる
ここで、以下のプログラムを実行して__closure__属性の値を見てみます。
def outer():
x, y, z = 0, 1, 2
print('xのメモリ番地: ', hex(id(x)))
print('yのメモリ番地: ', hex(id(y)))
def inner():
print('x = ', x)
print('y = ', y)
return inner
print("outer関数のclosure属性: ", outer.__closure__)
c_inner = outer()
c_inner()
cells_obj = c_inner.__closure__
print('c_inner関数のclosure属性: ', cells_obj)
'''
実行結果
outer関数のclosure属性: None
xのメモリ番地: 0x10c40eaa0
yのメモリ番地: 0x10c40eac0
x = 0
y = 1
c_inner関数のclosure属性: (<cell at 0x7fdb18097fd0: int object at 0x10c40eaa0>, <cell at 0x7fdb180d6040: int object at 0x10c40eac0>)
'''
c_inner関数のclosure属性をみると、セルオブジェクトが存在するメモリ番地と、整数オブジェクトのメモリ番地が表示されています。実行結果の2、3行目と比較すると、x、 yのメモリ番地と等しいことがわかります。
よって、セルオブジェクトはクロージャが用いるオブジェクトのメモリ番地を保持していて、クロージャはセルオブジェクトを介して間接的に外側のオブジェクトを参照しているといえます。
参考文献