Pythonのデコレータを作って理解する

# `@hoge`の意味がわからない

Pythonに触れていると次のようなコードを見ることがあります

``` python
@hoge
def fuga():
  return "piyo"
```

この`@hoge`がなにをしているのかよくわからなかったので調べてみました。

# デコレータ

`@hoge`はデコレータと呼ばれていて、さきほどのコードは次のコードの糖衣構文です([参考](https://docs.python.jp/3/glossary.html))。

```python
def fuga():
  return "piyo"

fuga = hoge(fuga)
```

つまり、次の二つのコードは等価です(実際にはだいたい等価([参考](https://docs.python.jp/3/reference/compound_stmts.html#function)))。

```python
@hoge
def fuga():
  return "piyo"
```

```python
def fuga():
  return "piyo"

fuga = hoge(fuga)
```

Pythonでは関数は[第一級オブジェクト](https://ja.wikipedia.org/wiki/%E7%AC%AC%E4%B8%80%E7%B4%9A%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88)として扱われるので、関数を引数にとったり、返り値として返すことができます。

つまり、ここでは`fuga`という関数をつくり、その`fuga`を関数を引数にとる`hoge`に渡して、`hoge`から返ってきた関数を`fuga`に割り当てているわけです。

すべてのデコレータはこのような糖衣構文であると理解して問題ないと思います。

# 基本のデコレータをつくる

上の説明をもとに基本的なデコレータをつくってみます。

```python
def decorator(function): # 関数を引数にとる

    def retfunc(*args): # うけとった関数を使いながらいろんな処理をする関数をつくる
        print("デコレートされたよ")
        return function(*args) # デコレートされる関数

    return retfunc # つくった関数を返す

@decorator
def fuga():
    return "piyo"

print( fuga() )
```

``` :出力
デコレートされたよ
piyo
```

デコレータである`decorator`は上で説明したように実際には関数です。

`decorator`の内部で関数を作って、その関数で受け取った関数を使って何かして、作った関数を返します。

`fuga`に渡すべき引数は`*args`で引き渡しています。

# 引数をとるデコレータ

次のような引数をとるデコレータもみかけるとおもいます。

```python
@decorator("arg1")
def fuga():
  return "piyo"
```

どうやったら作れるでしょうか。

デコレータに渡す引数を複数にしてやれば作れるでしょうか?

```python
def decorator(function, arg1): # デコレータに引数をとりたい

    def retfunc(*args):
        print("{}を受け取りました".format(arg1))
        print("デコレートされたよ")
        return function(*args)

    return retfunc

@decorator("arg1")
def fuga():
    return "piyo"

print( fuga() )
```

```:error
TypeError: decorator() takes exactly 2 arguments (1 given)
```

引数が足りないと怒られちゃいました。

デコレータが上で説明したような糖衣構文であることを考えると、このコードはインタプリタにはこんな感じに見えているはずです。

```python
fuga = decorator("arg1")(fuga)
```

引数が足りないと怒られるのは当然ですね。

ということは、`decorator("arg1")`の部分でさきほどの基本のデコレータを作って返す形にすれば大丈夫そうです。

```python
def decorator(arg1): # デコレータに引数をとりたい
    
    def real_decorator(function): # ここでベーシックなデコレータを作る
        
        def retfunc(*args):
            print("{}を受け取りました".format(arg1))
            print("デコレートされたよ")
            return function(*args)

        return retfunc
    
    return real_decorator

@decorator("arg1")
def fuga():
    return "piyo"

print( fuga() )
```
```:出力
arg1を受け取りました
デコレートされたよ
piyo
```
# クラスを使ったデコレータ

引数をとるデコレータは`fuga = decorator("arg1")(fuga)`の形を作ってやればうまく動いてくれました。

ということは、`decorator("arg1")`で初期化したオブジェクトに、関数を渡してcallするというアプローチでも動いてくれるでしょうか?

つまり

```pyhon
obj = decorator("arg1")
fuga = obj(fuga)
```

というアプローチでも動くのでしょうか?

動きます。

```python
class class_base_decorator:
    
    def __init__(self, arg1):
        self.arg1 = arg1
        print("{}で初期化".format(self.arg1))
        
    def __call__(self, function): # オブジェクトを関数のように呼ぶ
        
        def retfunc(*args):
            print("{}をつかってデコレートしています".format(self.arg1))
            print("デコレートされたよ")
            return function(*args)

        return retfunc
    
@class_base_decorator("arg1")
def fuga():
    return "piyo"

print( fuga() )
```

```:出力
arg1で初期化
arg1をつかってデコレートしています
デコレートされたよ
piyo
```

# メモ化するデコレータを作ってみる

n番目のフィボナッチ数を求めるプログラムを、デコレータをつかってメモ化をしてみます。フィボナッチ数を求めるプログラムとそのメモ化については[こちら](http://judge.u-aizu.ac.jp/onlinejudge/commentary.jsp?id=ALDS1_10_A)などを参照してください。

```:n番目のフィボナッチ数の定義
f(0) = 0,
f(1) = 1,
f(n) = f(n-1) + f(n-2) 　　　//  (n ≧ 2)
```

まずは素のフィボナッチ数を再帰で求める関数

```python
def fib(n):
    if n == 0 :
        return 0
    if n == 1:
        return 1
    return fib(n-1) + fib(n-2)
```
これをメモ化していきます

```python
class class_base_memoize:
    
    def __init__(self,function):
        self.memo = {} # メモ用の辞書を用意する
        self.function = function
        
    def __call__(self):
        
        def memoized(n):
            if n not in self.memo: # 辞書にメモされているか
                self.memo[n] = self.function(n) # されてなければメモする
            return self.memo[n] # メモの内容を返す

        return memoized

@class_base_memoize
def fib(n):
    if n == 0 :
        return 0
    if n == 1:
        return 1
    return fib(n-1) + fib(n-2)
```

メモ化できました。

上の例ではクラスをつかってメモ用の辞書を用意しましたが、Pythonにはクロージャがあって定義時の変数を覚えておいてくれるので([参照](http://www.lifewithpython.com/2014/09/python-use-closures.html))、もっとシンプルに次のように書けます。

```python
def memoize(function):

    memo = {} # memoizedは何度呼び出されても、関数定義時にあったmemoを見に行く

    def memoized(n):
        if n not in memo:
            memo[n] = function(n)
        return memo[n]

    return memoized

@memoize
def fib(n):
    if n == 0 :
        return 0
    if n == 1:
        return 1
    return fib(n-1) + fib(n-2)
```

# 参考

[Python Tips：Pythonでクロージャを使いたい - Life with Python](http://www.lifewithpython.com/2014/09/python-use-closures.html)
[用語集 — Python 3.6.3 ドキュメント](https://docs.python.jp/3/glossary.html)
[Pythonのデコレータを理解するための12Step - Qiita](https://qiita.com/_rdtr/items/d3bc1a8d4b7eb375c368#step8-%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%B8%E3%83%A3)
[Decorators With Arguments in Python – Scott Lobdell](http://scottlobdell.me/2015/04/decorators-arguments-python/)
[Katie Silverio Decorators, unwrapped How do they work PyCon 2017](https://www.youtube.com/watch?v=UBSyD1RkOX0)