Pythonのデコレータに固定引数を渡す別名をつける

# モチベーション

Python の変数付きデコレータの変数を固定させて別名を与えたいことがある。
今回は以下のような汎用デコレータ `tag` を対象にする。

```python:main.py--tagデコレータの実装と利用例
import functools


def tag(*tags):
    def _tag(f):
        @functools.wraps(f)
        def _wrapper(*args, **kwargs):
            res = f(*args, **kwargs)
            begins = ''.join([f'<{e}>' for e in tags])
            ends = ''.join(reversed([f'</{e}>' for e in tags]))
            return f'{begins}{res}{ends}'
        return _wrapper
    return _tag


@tag()
def test1():
    return 'test1'


@tag('p')
def test2():
    return 'test2'


@tag('p', 'span')
def test3():
    return 'test3'


if __name__ == '__main__':
    print(test1())
    print(test2())
    print(test3())
```

```bash:実行結果
$ pipenv run python main.py
test1
<p>test2</p>
<p><span>test3</span></p>
```

逐次全てを書くこともできるが、コンテンツが長くなる場合や記述上の業務ロジックが入ってくる場合は別名を与えたい。 例えば、`<h1>` で囲む部分を `@title` と書きたい、といったことが考えられる。
これをどのように実現するかを説明する。

なお、Pythonのバージョンは `3.8.2` , Ubuntu 18.04 上で実行。


# 実現方法

以下の通り。 必要部分のみ抜粋

```python:main.py
title = tag('h1')   # 利用時に引数を追加で利用しない場合


def with_p(*tags):  # 利用時に変数を追加で利用する場合
    return tag('p', *tags)


@title
def test4():
    return 'test4'


@with_p('span')
def test5():
    return 'test5'


if __name__ == '__main__':
    print(test4())
    print(test5())
```

```bash:実行結果
$ pipenv run python main.py 
<h1>test4</h1>
<p><span>test5</span></p>
```

このように、固定の引数を渡した別名のデコレータを作成することができた。


# なぜこのような方法になるのか

そもそもPythonの文脈におけるデコレータは、関数定義のための構文糖衣である。 [公式ドキュメント](https://docs.python.org/ja/3/reference/compound_stmts.html#function) の例を引用すると、

```python
@f1(arg)
@f2
def func(): pass
```

> は、だいたい次と等価です

```python
def func(): pass
func = f1(arg)(f2(func))
```

> ただし、前者のコードでは元々の関数を func という名前へ一時的に束縛することはない、というところを除きます。

デコレータとして利用できる項目は **関数を引数に取り、関数を返すcallable** である必要がある。 このような callable を「デコレータとして利用可能」とここでは呼ぶこととする。
実は `tag` 関数そのものは「デコレータとして利用可能ではない」(そのため、`@tag` とだけ書くと失敗する)。 なぜなら、この関数は「任意個数の引数を取り、「デコレータとして利用可能なcallable」を返す関数」であるためである。 この関数そのものはデコレータとして利用可能ではないが、**この関数の戻り値はデコレータとして利用可能** である。 そのため、`@tag` として利用するのではなく、`@tag()` のように呼び出して、その結果をデコレータとして利用する。

このように「デコレータとして利用可能」な型を意識すれば、別名を付けたり、一部に固定変数を渡す方法も見えてくる。

`tag('h1')` の評価結果はデコレータとして利用可能なので、`title = tag('h1')` とすれば、`@title` というデコレータを利用することができるようになる。
また、`def with_p(*args): ...` は 「任意個数の引数を取り、「デコレータとして利用可能なcallable」を返す関数」であり、型としては `tag` 関数と等しい。 そのため、`with_p()` で「デコレータとして利用可能なcallable」を得ることができるので、`@with_p()` というデコレータを利用することができるようになる。


## 関数の部分適用 functools.partial を使う
 
実は最初は `functools.partial` で部分適用すればいいのかと思っていたのだが、これをやると失敗した。

```python
h2 = functools.partial(tag, 'h2')


@h2
def test6():
    return 'test6'


if __name__ == '__main__':
    print(test6())
```

```bash
$ pipenv run python main.py 
Traceback (most recent call last):
  File "main.py", line 59, in <module>
    print(test6())
TypeError: _tag() missing 1 required positional argument: 'f'
```

これは `functools.partial` の仕様に起因する。 例えば、以下のような Haskell の例では、`add5` は `add` に1引数を部分適用した `Integer -> Integer` 型の関数である。 一方、`add 2 3` のように全ての引数を渡した時点で、その結果は `Integer` となり、結果が得られる。 ( [こちらを参考に一部引用](http://capm-network.com/?tag=Haskell-%E9%83%A8%E5%88%86%E9%81%A9%E7%94%A8) )

```haskell
add :: Integer -> Integer -> Integer
add x y = x + y
add5 = add 5
```

しかし、`functools.partial` は結果として [callableなpartialオブジェクトを返す。](https://docs.python.org/ja/3/library/functools.html#functools.partial)　そのため、**関数に全ての引数を適用したとしても結果が得られるのではなく、0引数で呼び出し可能なオブジェクトが返ってくる。**

```python
def add(x, y):
    return x + y


add23 = functools.partial(add, 2, 3)


if __name__ == '__main__':
    print(add23)
    print(add23())
```

```bash
$ pipenv run python main.py 
functools.partial(<function add at 0x7f2d3e5dfc10>, 2, 3)
5
```

つまり、`functools.partial(tag, 'h2')` の戻り値の型は `tag` と変わらないのである。`@tag` として使えないため `@tag()` のようにする必要があった通り、この場合は `@h2` ではなく、`@h2()` としてやらなければならない。 あるいは、`h2 = functools.partial(tag, 'h2')()` のようにしておけば、`@h2` として利用できる。


## おまけ

Chalice や Flask では `@app.route` のようにインスタンス内の関数を指定したデコレータがよく使われる。
これに固定値を含めた業務上の別名を付ける場合は、上記の通り型を意識して、

```python:app.py
def post(route, **kwargs):
    return app.route(route, methods=['POST'], **kwargs)


@post('/')
def index():
    return {'hello': 'world'}
```

のようにすることで別名を定義できる。


# まとめ

「デコレータとして利用可能な関数(callable)」を意識することで、より柔軟にコーディングができるようになった。


# 参考

- https://docs.python.org/ja/3/reference/compound_stmts.html#function
- https://qiita.com/_rdtr/items/d3bc1a8d4b7eb375c368