クロージャ

入門Python3 4.7.9


クロージャです。C#にもありますね、同じものです。

C#でも作ったカウンタを作るコードをPythonでも書いてみます。

def counter (i):
    def inner():
        ret = i
        i += 1
        return ret
    return inner

cnt1 = counter(1)
cnt2 = counter(10)

print(cnt1())
print(cnt1())
print(cnt1())

print(cnt2())
print(cnt2())
print(cnt2())

簡単ですね、さあ実行してみると・・・

$ py main.py
Traceback (most recent call last):
  File "main.py", line 12, in <module>
    print(cnt1())
  File "main.py", line 4, in inner
    ret = i
UnboundLocalError: local variable 'i' referenced before assignment

あれ・・・エラーが出ます。

エラー内容を調べるとどうやら、関数の外で定義された変数はローカル変数であり、関数の中で参照は出来るが変更はできないとのこと。

つまり上記実装でいうとi変数はinner関数の外で定義された変数なのでiを参照することができるけどiを変更することはできないってことなのです。

うーんじゃあどうすればいいのか。

要はiそのものを変更しなければ良いだけなのでiが参照してる何かなら変更しても問題はないということになります。

つまりcounter関数内でiを一旦リストに書き換えちゃえば良いということです。

def counter (i):
    i = [i] # リスト化する
    def inner():
        ret = i[0]
        i[0] += 1 # iは書き換えてないからエラーにならない
        return ret
    return inner

cnt1 = counter(1)
cnt2 = counter(10)

print(cnt1())
print(cnt1())
print(cnt1())

print(cnt2())
print(cnt2())
print(cnt2())
$ py main.py
1
2
3
10
11
12

inner関数内でi変数そのものは変更しておらず、あくまでもi変数が参照してるリストを書き換えてるだけなのでエラーにならず期待通りに動くというわけです。

しかしせっかくの便利なクロージャなのにこういったちょっとバッドノウハウ的な使い方しかできないのはちょっと残念ですよね・・・


・・・と思いきや、この話はPython2の時のやり方でPython3からはこういったケースの時に値を変更できるようにする構文が追加されています。すごーい。

それがnonlocal文です。

def counter (i):
    def inner():
        nonlocal i # iは変更しますよエラーにしないでね
        ret = i
        i += 1     # 変更してもエラーにならない
        return ret
    return inner

これで期待通りになりました。良いですねぇ、クロージャは便利なので今後も使っていきます。