ホームページ  >  記事  >  バックエンド開発  >  パラメータを備えた完全に型指定された Python デコレータ

パラメータを備えた完全に型指定された Python デコレータ

WBOY
WBOY転載
2023-04-13 17:58:061329ブラウズ

パラメータを備えた完全に型指定された Python デコレータ

この短い記事に示されているコードは、契約によって設計された私の小さなオープンソース プロジェクトから抜粋されたもので、型付きデコレーターを提供します。デコレータは非常に便利な概念であり、オンラインで多くの情報を見つけることができます。簡単に言えば、装飾された関数が呼び出されるたびに (前後に) コードを実行できるようになります。このようにして、関数のパラメーターや戻り値の変更、実行時間の測定、ログの追加、実行時の型チェックの実行などを行うことができます。デコレータはクラス用に作成することもでき、別のメタプログラミング アプローチ (attrs パッケージで行われるような) を提供することもできることに注意してください。

最も単純な形式では、デコレータ定義は次のコードのようになります。

def my_first_decorator(func):
 def wrapped(*args, **kwargs):
 # do something before
 result = func(*args, **kwargs)
 # do something after
 return result
 return wrapped
@my_first_decorator
def func(a):
 return a

上記のコードは、ラップされた入れ子関数が定義されると、その関数がどこかで使用されている限り、周囲の変数に関数内でアクセスしてメモリに保存できるためです (関数型プログラミング言語ではこれをクロージャと呼びます)。 )。

シンプルですが、これにはいくつかの欠点があります。最大の問題は、装飾された関数が以前の関数名 (inspect.signature で確認できます)、ドキュメント文字列、さらにはその名前さえも失うことです。これらはソース コード ドキュメント ツール (sphinx など) の問題ですが、場合によっては、これは、標準ライブラリの functools.wraps デコレータを使用すると簡単に解決できます。

from functools import wraps
from typing import Any, Callable, TypeVar, ParamSpec
P = ParamSpec("P") # 需要python >= 3.10
R = TypeVar("R")
def my_second_decorator(func: Callable[P, R]) -> Callable[P, R]:
 @wraps(func)
 def wrapped(*args: Any, **kwargs: Any) -> R:
 # do something before
 result = func(*args, **kwargs)
 # do something after
 return result
 return wrapped
@my_second_decorator
def func2(a: int) -> int:
 """Does nothing"""
 return a
print(func2.__name__)
# 'func2'
print(func2.__doc__)
# 'Does nothing'

この例では、型アノテーションを追加しました。アノテーションと型ヒントは Python に対して行われます。追加。可読性の向上、IDE でのコード補完、大規模なコード ベースの保守性はほんの一例です。上記のコードはほとんどのユースケースをすでにカバーしているはずですが、デコレータをパラメータ化することはできません。関数の実行時間を、一定の秒数を超えた場合にのみ記録するデコレータを作成することを検討してください。この数値は、装飾された関数ごとに個別に構成できる必要があります。指定しない場合は、デフォルト値を使用し、使いやすくするためにデコレーターを括弧なしで使用する必要があります。

@time(threshold=2)
def func1(a):
...
# No paranthesis when using default threshold
@time
def func2(b):
...

2 番目のケースで括弧を使用できる場合、または指定しないでください。パラメータのデフォルト値がまったくない場合は、このレシピで十分です:

from functools import wraps
from typing import Any, Callable, TypeVar, ParamSpec
P = ParamSpec("P") # 需要python >= 3.10
R = TypeVar("R")
def my_third_decorator(threshold: int = 1) -> Callable[[Callable[P, R]], Callable[P, R]]:
 def decorator(func: Callable[P, R]) -> Callable[P, R]:
 @wraps(func)
 def wrapper(*args: Any, **kwargs: Any) -> R:
 # do something before you can use `threshold`
 result = func(*args, **kwargs)
 # do something after
 return result
 return wrapper
 return decorator
@my_third_decorator(threshold=2)
def func3a(a: int) -> None:
...
# works
@my_third_decorator()
def func3b(a: int) -> None:
...
# Does not work!
@my_third_decorator
def func3c(a: int) -> None:
...

3 番目のケースをカバーするには、使用可能な Select パラメータを追加するだけでなく、実際にはそれ以上のことを実行できるパッケージ、つまりラップとデコレータがあります。品質は非常に高いですが、かなりの複雑さが追加されます。 Wrapt で装飾された関数を使用すると、リモート クラスターで関数を実行するときにシリアル化の問題がさらに発生しました。私の知る限り、どちらも完全に型指定されていないため、静的型チェッカー/リンター (mypy など) は厳密モードでは失敗します。

私が独自のパッケージに取り組み、独自のソリューションを作成することにしたとき、これらの問題を解決する必要がありました。再利用はしやすいがライブラリ化が難しいパターンとなります。

標準ライブラリのオーバーロードされたデコレータを使用します。このようにして、同じデコレータをパラメータのないデコレータで使用するように指定できます。それ以外は、上記の 2 つのスニペットを組み合わせたものです。このアプローチの欠点の 1 つは、すべてのパラメーターをキーワード引数として指定する必要があることです (これにより、最終的に読みやすさが向上します)

from typing import Callable, TypeVar, ParamSpec
from functools import partial, wraps
P = ParamSpec("P") # requires python >= 3.10
R = TypeVar("R
@overload
def typed_decorator(func: Callable[P, R]) -> Callable[P, R]:
...
@overload
def typed_decorator(*, first: str = "x", second: bool = True) -> Callable[[Callable[P, R]], Callable[P, R]]:
...
def typed_decorator(
 func: Optional[Callable[P, R]] = None, *, first: str = "x", second: bool = True
) -> Union[Callable[[Callable[P, R]], Callable[P, R]], Callable[P, R]]:
 """
Describe what the decorator is supposed to do!
Parameters
----------
first : str, optional
First argument, by default "x".
This is a keyword-only argument!
second : bool, optional
Second argument, by default True.
This is a keyword-only argument!
"""
 def wrapper(func: Callable[P, R], *args: Any, **kw: Any) -> R:
 """The actual logic"""
 # Do something with first and second and produce a `result` of type `R`
 return result
 # Without arguments `func` is passed directly to the decorator
 if func is not None:
 if not callable(func):
 raise TypeError("Not a callable. Did you use a non-keyword argument?")
 return wraps(func)(partial(wrapper, func))
 # With arguments, we need to return a function that accepts the function
 def decorator(func: Callable[P, R]) -> Callable[P, R]:
 return wraps(func)(partial(wrapper, func))
 return decorator

後で、パラメーターのデコレーターなしで個別に使用できるようになります。

@typed_decorator
def spam(a: int) -> int:
 return a
@typed_decorator(first = "y
def eggs(a: int) -> int:
 return a

このパターンには確かにある程度のオーバーヘッドがありますが、メリットはコストを上回ります。

原文:

https://www.php.cn/link/d0f82e1046ccbd597c7f2a7bfba9e7dd

以上がパラメータを備えた完全に型指定された Python デコレータの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事は51cto.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。