Pythonのクロージャ理解する

少し複雑なデコレータを実装したくなったけど、クロージャの挙動ちゃんと理解してないなと思いFluent Pythonで復習しました。
使ったのは第7章5節と6節です。

Fluent Python ―Pythonicな思考とコーディング手法

Fluent Python ―Pythonicな思考とコーディング手法

関数の外部で定義された非グローバルな変数うにどのようにアクセスしているか確認します。
例として挙がっていたのは値が除々に追加されているリストの平均値を計算するようなavg関数です。

まずはクラスを使って実装すると下記のようになります。

class Averager():


  def __init__(self):
    self.series = []

  def __call__(self, new_value):
    self.series.append(new_value)
    total = sum(self.series)
    return total/len(self.series)

if __name__ == "__main__":
  avg = Averager()
  print(avg(10))
  print(avg(11))
  print(avg(12))
実行結果:
10.0
10.5
11.0

このときavgはAveragerのインスタンス。
次は同様のものを関数型で実装してみる。

def make_averager():
  series = []

  def averager(new_value):
    series.append(new_value)
    total = sum(series)
    return total / len(series)

  return averager

if __name__ == "__main__":
  avg = make_averager()
  print(avg(10))
  print(avg(11))
  print(avg(12))
実行結果:
10.0
10.5
11.0

このときのavgは make_averager()の返り値であるaverager関数。
avg関数はseriesというaveragerの外で定義されている変数を参照している.

次に、totalやlen(series)を毎回計算するのは効率が悪いので、これまでの値の総和や要素数のみを保持するように書き換えます。

def make_averager():
  count = 0
  total = 0

  def averager(new_value):
    count += 1
    total += new_value
    return total / count

  return averager

if __name__ == "__main__":
  avg = make_averager()
  print(avg(10))
  print(avg(11))
  print(avg(12))

するとエラーがでる。

実行結果:
Traceback(most recent call last):
 ...
UnboundLocalError: local variable 'count' referenced before assignment

ここでcountとtotalはaveragerの中で再代入されているのでローカル変数扱いとなってしまいます。
参照はOk,再代入はできない。
けど、nonlocal宣言を使うとこのエラーを回避できる。

def make_averager():
  count = 0
  total = 0

  def averager(new_value):
    nonlocal count, total
    count += 1
    total += new_value
    return total / count

  return averager

if __name__ == "__main__":
  avg = make_averager()
  print(avg(10))
  print(avg(11))
  print(avg(12))
実行結果:
10.0
10.5
11.0

クロージャの挙動なんとなく理解できたんで、頑張ってデコレータの実装します。