対象読者
- pythonでデコレータを作成したことがある方
- pythonで型注釈を活用したコードを書いている方
- python3.5以上(多分3.5以上で有効なコードです)
はじめに
pythonのデコレータはよく利用すると思いますが、デコレートしてしまうと、元関数の引数情報が失われてしまったり、一体何が返っているのか分からないというデメリットがあります。
最近では型注釈が発達してきて、この問題を解決できるようになってきているので、実装例を紹介します。
確認はVSCode(Pylance + mypy)でやりましたが、IDEや環境次第では型情報の認識の仕方が異なるかもしれません。
実装例
まず、基本形から。
次のように、型変数を定義して、それを戻り値の型にしてあげれば関数の情報を伝搬できます。
from typing import Callable, TypeVar
F = TypeVar("F", bound=Callable)
def decorator(func: F) -> F:
...
return func
@decorator
def func(num1, num2):
return num1 + num2
次に、クラスでデコレータを作成する方法を紹介します。
ただし、この方法は一般的な方法じゃないので、こういうことができるよって知識レベルに留めておきましょう。
ご利用は、自己責任でお願いします。
from functools import update_wrapper
from typing import Any, Callable, Generic, TypeVar
F = TypeVar("F", bound=Callable)
class FuncWrapper(Generic[F]):
def __init__(self, func: F):
update_wrapper(self, func) # 元の関数を__wrapped__に保持する。など、ラッパー作成のヘルパー関数
# update_wrapperだけでは不十分な情報を保持する
self.__code__ = func.__code__
self.__defaults__ = func.__defaults__
self.__kwdefaults__ = func.__kwdefaults__
def __getattr__(self, name):
return getattr(self.__wrapped__, name) # 未定義の属性は元の関数を参照しにいく
@property
def __call__(self) -> F:
return self.__wrapped__
@property
def __class__(self):
return self.__wrapped__.__class__ # とりあえず、返すクラスも擬態する
@FuncWrapper
def func(num1, num2):
return num1 + num2
Genericで型変数を保持し、それをどこかで利用します。
キモは、__call__
をメソッドじゃなくて、プロパティにしてしまったことですね。
関数の引数と戻り値情報を取り出すのは至難の業ですし、取り出したとしても__docstring__が失われてしまうので、これが一番簡単だと思います。
なお、FuncWrapperをinspectモジュールで一通り分析しており、確認した範囲では元関数と同じ判定結果となっています。
もちろん、擬態しているだけなので、何かしら副作用が生じる可能性はあります。
pep612 Parameter Specification Variables
デコレータの型注釈でみんな悪戦苦闘しているようで、引数情報をもっと簡単に扱うための機能導入が検討されています。
さいごに
結果だけ見れば簡単なことでしたが、__call__
をプロパティにするという発想が中々できず、結構時間を費やしました。。。