pythonのdecorator周りのtype hintについて調べる
疑問点
decorator周りのtype hintで引数にとった関数のtype hintをdecorator側のtype hintが上書きしないようにできないんだろうか?
python3.10を前提に調べる。
何に使うんだろうと思ってたけど、ハマってる事案の解決策になりそうなので調べる。
参考資料
関数のシグネチャの type hint として collections.abc.Callable がある。これは PEP484 で定義されてる。
具体的には、以下のように使う。
from collections.abc import Callable
def hoge(a: int = 10, b: dict[str, int] | None = None ) -> int:
return 1
def fuga(fn: Callable[[int, dict[str, int] | None], int]) -> None:
fn()
ただ、関数を引数として取るときに具体的な型がわからないこともある。
これを解決する方法は、ParamSpec が追加される前は、elipsis を使ってtype hint を書くことだったらしい。
from typing import TypeVar
from collections.abc import Callable
def hoge(a: int, b: dict[str, int]) -> int:
return 1
def fuga(fn: Callable[..., int]) -> None:
fn()
ただし、この方法だと decorator の定義の時に以下のような時に問題が生じる。
例は参考資料3. を参考にして elipsisを使った定義に変更してある。
from typing import Any, TypeVar
from collections.abc import Callable
import logging
AnyCallableT = TypeVar("AnyCallableT", bound=Callable[..., Any])
def add_logging(f: AnyCallableT) -> AnyCalla![](https://storage.googleapis.com/zenn-user-upload/2f116dc0c9df-20240124.png)
bleT:
'''A type-safe decorator to add logging to a function.'''
def inner(*args, **kwargs) -> Any:
logging.info(f'{f.__name__} was called')
return f(*args, **kwargs)
return inner
@add_logging
def add_two(x: float, y: float) -> float:
'''Add two numbers together.'''
return x + y
この例では、inner
関数の引数は Any とされるべきになってしまうことと、 add_logging
の内部で typing.cast
関数を呼ぶか、return inner のエラーを無視するように静的解析に教える必要がある。
実際にpyrightにかけて、解析してみるとアサインできないエラーが起きるのでcast/type: ignoreが必要になりそうなことがわかる。(使用pyright version: 1.1.329)
というわけで、以上のような時に問題になる部分を解決するのが ParamsSpec というらしい。
エディタに、参考資料3.の例をそのまま貼り付けて add_two
関数を呼び出してみた。
さっきの課題点クリアしてそう。
ここまで調べてさっきの大元の問題は従来の方法だと解決してるかチェックしてなかったのに気づいたので従来の方法でもデコレートされた関数が呼び出し時にちゃんと関数側の type hintで解析されてるかチェックしてみた
ちゃんとチェックされてそう。自分の疑問点はどちらの方法でもクリアできそうだけど ParamSpec
使えるなら使って解決した方がエラー握り潰さず済んで良さそう。
という訳で、上のリンク先の capture_lambda_handler
メソッドの lambda_handler
の type hintはこのファイルでインポートされてる AnyCallableT
を使う方法の方が、event_source とかで引数を事前にパースしておく時に type hint を parse した型で書けるの良くないか?という疑問点が晴れたのでした。(他に見落としててこうなってるかもなのでもう少し調査してみる)
python3.9をベースにしてるらしいので ParamSpec
は使えない。
やるなら AnyCallableT
で回避する方法が良さそう(シンボルも同ファイル内に import されてる)