pythonにおける高階関数と@デコレータについて（Python中級）

## はじめに
Pythonを使っている皆さん、@デコレータというのはご存知でしょうか。Djangoのようなwebフレームワークに触れたことがある方であれば「ログインが必要な動作を行う関数の前に`@login_required`をつける」というようなことを「おまじない」として覚えた方もいるかもしれません。この@デコレータの意味を学び、さまざまなプログラムに応用してみよう、というのが今回の趣旨です。
なお、説明には誤りが含まれている可能性があります。もし誤りを見つけた場合にはご指摘いただけると助かります。

## 想定する読者像
python初級者～中級者。「引数」、「返り値（戻り値）」の概念や例外処理`try-except`は理解できていることを前提とします。

## 最もシンプルな例

```python
@funcA
def funcB():
    print("funcB")

funcB()
```
この時、このプログラムは`funcA(funcB)()`という意味になります。さて、この式の意味を正しく読み取ることができるでしょうか。
- なぜ`funcA`の引数に関数である`funcB`が入っているのか？
- なぜ引数を表す`()`がfuncAの後ろに２つ並んでいるのか？

上のような疑問を持った方もいるかもしれません。ではここで冷静になって考えてみましょう。`funcA`の引数と返り値はいったいどんなものでしょうか？
`funcA`の直後の`()`に関数であるfuncBが入っているのですから、今までのプログラミングのルールに従えば、`funcA`は「関数を引数に取る関数」といえるでしょう。そして`funcA(funcB)`はその直後に`()`があるため、`funcA(funcB)`それ自体も関数になっているということができます。すなわち、`funcA`は「関数を引数にとり関数を返す関数」になっていることが分かります。

関数型プログラミングの経験がある方であればすぐにピンとくるでしょうが、funcAはいわゆる**高階関数**となっているのです。
ではここで一度、高階関数について確認しておきましょう。


## 高階関数について

高階関数、などと大層な名前がついていますが、要するに引数や返り値が関数になっているというだけで難しいことはありません。ここでは分かりやすい例を２つ示します。

### 高階関数の例１：総和計算Σ
文系の方でも高校数学で触れたことはあるでしょう、総和を表すΣです。これをpythonの関数として表してみましょう。例えばその計算は以下のようになります。

```math
\sum_{n=1}^{3} (2n+1) = (2×1+1)+(2×2+1)+(2×3+1) = 15 \\


\sum_{n=1}^{5} n^2 = 1^2+2^2+3^2+4^2+5^2 = 55

```
例を見ればわかるように、Σの計算には「１から５まで」のような範囲の指定の他に、$n^2$のような、どのような演算を行うかについての指定も必要になります。すなわちΣの計算を行う関数には、下に示すように整数`a`、`b`の他に、関数`f`も必要になるのです。

```math
\sum_{n=a}^{b} f(n)\\


```

では、実際の実装を見てみましょう。

```python
def sigma(a,b,f):
    answer=0
    for i in range(a,b+1):
        answer+=f(i)
    return answer

def square(n):
    return n**2

print(sigma(1,5,square))
```

これを実行すると、`55`が出力されます。
ただ、この方式だとsigmaの計算をするときに毎回fに入れるための関数を作らなければなりません。fが複雑な処理を行う関数ならば仕方ないのですが、「２乗する」とか「２倍して１を足す」くらいはもう少し簡潔に書きたいですね。
そこで使えるのが「匿名関数」という仕組みです。話がそれるため詳細は省略しますが、ラムダ式と呼ばれるものを用いて以下のように書くことができます。

```python
def sigma(a,b,f):
    answer=0
    for i in range(a,b+1):
        answer+=f(i)
    return answer

print(sigma(1,5,lambda n:n**2))
```

さて、次は関数を返す高階関数の例を挙げてみます。

### 高階関数の例２：関数の反復実行

次は簡単な高階関数として、関数`f`と正の整数`n`を引数として、fをn回繰り返す関数を返す関数を実装します。

```python
def repeat(f,n):
    def wrapper(*args, **kwargs):
        for i in range(n):
            f(*args, **kwargs)
    return wrapper

#使用例
def func1(text):
    print(text)

func2=repeat(func1,3)
func2("Hello")
```

さきほどよりも難しそうな雰囲気がすると思いますが、一つ一つ確認していきましょう。
まず、`f`には何らかの関数が、`n`には繰り返し回数を表す正の整数が与えられます。
そして、関数の中で定義した関数wrapperが返り値として返されています。
使用例においてはこの返り値としての関数がfunc2に格納され、引数`"Hello"`で呼び出されています。この呼び出し時の引数は`func1`の引数と同じになります。すなわち、関数repeatを定義する時点では、関数wrapperをどのような引数を取る関数として定義するべきかは分かりません。
そこで`*args`や`**kwargs`を使用します。これは可変長引数と呼ばれるもので、引数の個数が可変である関数を定義できます。
このwrapper関数を、関数fに受け取った引数をそのまま与えることをn回繰り返すものとして定義すれば完了です。冷静に考えればそこまで難しくはないでしょう。


<details>
<summary>可変長引数について（詳細）</summary>



```python
def func3(*args, **kwargs):
    print(args)
    print(kwargs)

func3("Hello", 1, a=2, b=3, c="World")
```
このときの出力は以下のようになります。

```
('Hello', 1)
{'a': 2, 'b': 3, 'c': 'World'}
```

すなわち、`"Hello"`や`1`のような位置引数は`*args`にタプルの形で、`a=2`や`c="World"`のようなキーワード引数は`**kwargs`に辞書の形でそれぞれ格納されます。
なお`arg`や`kwargs`というのは単なる慣用名であり、アスタリスクの数さえあっていれば他の名前でも構いません。


</details>

## @デコレータの使用

では@デコレータを実際に使用する例を見てみましょう。

### @デコレータの例１：例外処理を付加する

まずは任意の関数をエラーハンドリングで修飾して返す関数を見てみましょう。

```python
def tryanderror(func):
    def wrapper(*args, **kwargs):
        try:
            func(*args, **kwargs)
        except Exception as e:
            print("エラーが発生しました。")
            print(f'type:{type(e)}')
            print(f'args:{str(e.args)}')
    return wrapper

@tryanderror
def plus(value):
    value = value + 1
    print(value)
    return value

plus("Hello")
```

さて、高階関数の例２が理解できていれば関数tryanderrorを理解するのはそれほど難しくないはずです。もし`try-except`構文の意味が理解できていない場合はそちらを先に学習することをお勧めします。
さて、関数funcは受け取った値に１を加えて返す関数ですが、もし受け取った値が文字列であればエラーとなってしまいます。そのときにエラーによる異常終了を引き起こさず、エラー内容を出力に通知するために@デコレータが使われています。
初めに説明したように、このプログラムは以下と等価です。

```python
def tryanderror(func):
    def wrapper(*args, **kwargs):
        try:
            func(*args, **kwargs)
        except Exception as e:
            print("エラーが発生しました。")
            print(f'type:{type(e)}')
            print(f'args:{str(e.args)}')
    return wrapper

def plus(value):
    value = value + 1
    print(value)
    return value

tryanderror(plus)("Hello")
```

さて順番に解釈していきましょう。まず`tryanderror(plus)`の返り値は関数wrapperです。`tryanderror`引数`func`に`plus`が代入されているので、具体的な関数の定義は以下のようになります。

```python
def wrapper(*args, **kwargs):
    try:
        plus(*args, **kwargs)
    except Exception as e:
        print("エラーが発生しました。")
        print(f'type:{type(e)}')
        print(f'args:{str(e.args)}')
```

この関数に対して引数`"Hello"`を与えている、と考えればもうそれほど難しくないでしょう。
`plus("Hello")`が実行され、途中で型エラーが発生し`except`の中でそれが出力される、というだけです。
以上のようにして、関数を受け取り関数を返す関数`tryanderror`と`@デコレータ`を用いることで任意の関数に手軽に例外処理を付加できるようになったことが分かるでしょう。

ですがこの方法だけだと１つ問題があります。それは、高階関数の章で述べたような関数の反復実行を@デコレータで実装しようとした場合、関数の反復回数`n`はどこで与えればよいのか、という問題です。これには、引数を受け取る@デコレータを用いる必要があります。

### @デコレータの例２：関数の反復実行

まずはサンプルコードを見てみましょう。

```python
def repeat(times):
    def repeat_f(func):
        def wrapper(*args, **keywords):
            for i in range(times):
                func(*args, **keywords)
        return wrapper
    return repeat_f

@repeat(10)
def sample(text):
    print(text)

sample("Hello")
```

@デコレータを使用している部分は、先ほどまで関数名のみが書かれていた部分が`@repeat(10)`のように引数を含む形に変わっています。それでも本質は変わりません。一番最初の例における`funcA`の部分を`repeat(10)`に置き換えるだけにすぎません。すなわち、`sample("Hello")`は以下と等価になります。

```python
repeat(10)(sample)("Hello")
```

順番に意味を理解していきましょう。まず、`repeat(10)`の返り値は、関数`repeat`の定義を見ると関数`repeat_f`であることが分かります。`times`に`10`が代入されているので、返り値`repeat_f`の定義は以下のようになります。

```python
def repeat_f(func):
    def wrapper(*args, **keywords):
        for i in range(10):
            func(*args, **keywords)
    return wrapper
```
さて続いてこの関数に対して引数として`sample`を与えたときの返り値は`wrapper`です。この定義は、`func`に`sample`を代入しているため以下のようになります。

```python
def wrapper(*args, **keywords):
    for i in range(10):
        sample(*args, **keywords)
```
この関数`wrapper`が`repeat(10)(sample)`の返り値です。あとはこの関数に引数`"Hello"`が与えられるため、結果として関数`sample`が引数を`"Hello"`として１０回実行される形となります。

ちなみに、この@デコレータの引数を関数定義時ではなく関数実行時に与えたい場合は、例えば以下のようにして関数を定義することで実現できます。

```python
def sampletimes(times,text):
    @repeat(times)
    def sample(text):
        print(text)
    sample(text)

sampletimes(10,'hello')
```


## 最後に（+練習問題）

無事に理解できたでしょうか？　@デコレータは理解してしまえばそれほど難しいものでもありません。自分のプログラムで使う機会があまりなかったとしても、これをきちんと理解しておくことで他人の書いたコードで不意に出くわしたときに面食らわなくて済むようになるでしょう。うまく使いこなせるようになれば便利な機能でもありますから、中級プログラマを目指す方はぜひ理解しておきましょう。

#### 練習問題

__整数型の変数`interval`と`times`を受け取り、`interval`秒間隔で`times`回関数を定期実行できるように関数を修飾できるデコレータを定義し、実際に使ってみましょう。ただし、デコレータの関数名は`sync`とすること。__
可能ならば、対象となる関数の実行時間が`interval`よりも小さい場合、実行間隔時間と`interval`の誤差が0.1秒以内になるように工夫してください。正しく実装できているか確認したい場合は、`sync`関数の下に以下のプログラムを貼り付けて実行してみましょう。`Assertion Error`が出ていなければ誤差上限をクリアできています。

ヒント：関数の実行自体にも時間がかかるので、単純に関数の実行が終わってから`interval`秒待つのでは誤差がうまれてしまいます。

```python:テストコード
import time
nowtime_ = time.time()
already_ = False
@sync(interval = 2,times = 5)
def test():
    global nowtime_,already_
    s = 0
    for i in range(10000000):
        s += i
    if already_:
        elasped = time.time()-nowtime_
        assert (1.9 < elasped and elasped < 2.1)
    nowtime_ = time.time()
    already_ = True
test()
```
<details>
<summary>解答例</summary>

解答はあくまで一例です。また、実行環境によってはこのプログラムでも誤差上限がクリアできない場合があるかもしれません。（おそらく大丈夫なはずですが）

```python:解答例
import time
def sync(interval, times):
    def _sync(f):
        def repeat_f(*args, **keywords):
            print("START")
            cnt = 0
            start = time.time()
            while cnt < times:
                cnt += 1
                while time.time()-start < cnt * interval:
                    time.sleep(0.05)
                v = f(*args, **keywords)
            print("END")
            return v
        return repeat_f
    return _sync
```


</details>