Python の デコレータとクロージャ

◯ いつ使うの?



複数の関数、メソッドに共通した
前処理と後処理を追加したいとき。


◯ 理解するときのポイント



デコレーターは、
デコレートされた関数を返す。



◯ サンプルコード①

pre do, post do を前後に出力するデコレータ。

>>> @decorator
... def func(a, b):
...   print(a + ", " + b + "!")
... 
>>> func("Hello", "world")
pre do
Hello, world!
post do
>>>



全体はこんな感じ。

def decorator(func):
  def decorated_func(*args, **kwargs):  
    print("pre do")        # 前処理
    func(*args, **kwargs)  # func の処理
    print("post do")       # 後処理
  # ポイント
  # デコレーターは、デコレートされた関数を返す。
  return decorated_func


# 書き方1
@decorator
def func(a, b):
  print(a + ", " + b + "!")

func("Hello", "world")
# pre do
# Hello, world!
# post do


# 書き方2
def func(a, b):
  print(a + ", " + b + "!")

decorated_func = decorator(func)

decorated_func("Hello", "world")
# pre do
# Hello, world!
# post do

 

◯ サンプルコード② デコレータに引数を渡す場合

True なら実行する。

>>> @create_decorator(True)
... def func(a, b):
...   print(a + ", " + b + "!")
... 
>>> func("Hello", "world")
pre do
Hello, world!
post do
>>> 



False なら何もしない。

>>> @create_decorator(False)
... def func(a, b):
...   print(a + ", " + b + "!")
... 
>>> func("Hello", "world")
Hello, world!
>>> 



全体はこんな感じ。

def create_decorator(param):             # 0. デコレータを作る関数
  def decorator(func):                   # 1. デコレータ(デコレートする関数)
    def decorated_func(*args, **kwargs): # 2. デコレートされた関数
      if param:
        print("pre do")
        func(*args, **kwargs)
        print("post do")
      else:
        func(*args, **kwargs)
    return decorated_func
  return decorator



#
# 書き方1
#
@create_decorator(True)
def func(a, b):
  print(a + ", " + b + "!")

func("Hello", "world")
# pre do
# Hello, world!
# post do


#
# 書き方2
#
def func(a, b):
  print(a + ", " + b + "!")

# 1. デコレータを作る
decorator = create_decorator(False)

# 2. デコレートする
decorated_func = decorator(func)

# 3. デコレートされた関数
decorated_func("Hello", "world")
# Hello, world!

closure, 関数閉包

「サンプルコード② デコレータに引数を渡す場合」では create_decorator で作られた decorator は True か False の状態を持っていました。この状態を保持するために create_decorator 関数が実行されたときに生成された名前空間を利用しました。

理解するために関数が実行されたときに生成された名前空間を利用して、カウンタを作って見ました。

def create_counter():
  c = 0
  def counter():
    nonlocal c  # 1つ外側の変数 c を参照する。
    c = c + 1
    return c
  return counter

counter = create_counter()
counter()  # 1
counter()  # 2
counter()  # 3
counter()  # 4
counter()  # 5

nonlocal 文は、列挙された識別子がグローバルを除く一つ外側のスコープで先に束縛された変数を参照するようにします。
7.13. nonlocal 文



これの何がすごいのかというと、関数なのに一切外側の名前空間を使わずに状態を持っているということです。

>>> counter = create_counter()
>>> counter()
1
>>> counter()
2
>>> counter()
3
>>> counter()
4
>>> counter()
5
>>> 



さて、ここで問題です。 counter2 の実行結果は何が出力されると思いますか? 1, 2, 3, 4, 5 でしょうか?それとも 6, 7, 8, 9, 10 でしょうか?

>>> counter1 = create_counter()
>>> counter1()
1
>>> counter1()
2
>>> counter1()
3
>>> counter1()
4
>>> counter1()
5
>>> 
>>> counter2 = create_counter()
>>> counter2()
?
>>> counter2()
?
>>> counter2()
?
>>> counter2()
?
>>> counter2()
?
>>>



答えは、次の通りです。新しく 1 からカウントされています。このことから、関数が実行されるたびに新しい名前空間が生成されていることがわかります。当たり前と言えば、当たり前ですね。

>>> counter1 = create_counter()
>>> counter1()
1
>>> counter1()
2
>>> counter1()
3
>>> counter1()
4
>>> counter1()
5
>>> 
>>> counter2 = create_counter()
>>> counter2()
1
>>> counter2()
2
>>> counter2()
3
>>> counter2()
4
>>> counter2()
5
>>>



関数が実行されたときに生成された名前空間は外からは、見ることができません。そのため閉じて包まれた名前空間です。この閉じて包まれた名前空間を持つ関数を、関数閉包, closure と呼びます。

クロージャクロージャー、英語: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。 ... 中略 ... 引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。
クロージャ - Wikipedia

def create_counter():
  # 1. 自身が定義された環境
  c = 0
  def counter():
    # 2. 実行時の環境
    nonlocal c  
    c = c + 1
    return c
  return counter



例えば、どうあがいても外側から関数 f のローカル変数 a を参照することはできません。

>>> def f():
...   a = 100
... 
>>>
>>> # 関数オブジェクト f の名前空間にある a
>>> f.a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'a'
>>> 
>>> # f() の返り値である None の名前空間にある a
>>> # (return を書かないと None が暗黙的に返される)
>>> f().a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'a'
>>> 



反対にモジュールやクラスの名前空間は参照できる。

>>> import math
>>> math.pi
3.141592653589793
>>>
>>> class C(object):
...   a = 100
... 
>>> C.a
100
>>>

クロージャを使って擬似乱数生成

簡単な擬似乱数を生成する関数を用意しました。
線形合同法 - Wikipedia

def linear_congruential_generators(a, x, b, m):
    def _():
        nonlocal x
        x = (a * x + b) % m
        return x
    return _


random = linear_congruential_generators(48271, 8, 0, 2**31 - 1)

random()  # 386168
random()  # 1460846352
random()  # 1741224500

デコレータのもう少しちゃんとした理解

備忘録的な形でまとめました。
ちゃんとした理解するには、
これがわかりやすい気がします。