mypyでPythonの関数定義を型チェック、できるけど...
implements
を作ってみた
関数定義をチェックするモチベーション
dependency_injector
を使って関数をDIしていくコンテナを作っていたんですが、Djangoを使っている都合上モデルのimport
が発生するコードをDIコンテナ定義ファイルからimport
できないという制約がありました。そこで一旦providers.Callable
でごまかしておいてAppConfig
のready
フック内で関数の実態を詰め込んでたんですが、dependency_injector
の型が結構ルーズなので実際に走らせてみないと関数シグネチャの不一致を見落としたり、そもそもdependency_injector
がかなりcrypticなメッセージと共に落ちる(コンテナの書き方を間違えると普通にセグフォルで落ちたりします、このご時世に?)のでなんとかしたいという思いが発端でした。
そんなこんなで「これくらいならmypyでなんとかできるのでは?」と思い立ったのがきっかけでした。結構すんなり実装できてそれっぽいことはできる...
implements
の実装
from functools import wraps
from typing import Callable, ParamSpec, Protocol, Type, TypeVar
Params = ParamSpec('Params')
Return = TypeVar('Return', covariant=True)
class CallableProtocol(Protocol[Params, Return]):
def __call__(self, *args: Params.args, **kwargs: Params.kwargs) -> Return: ...
T = TypeVar('T')
def implements(protocol: Type[CallableProtocol[Params, Return]]) -> Callable[[CallableProtocol[Params, Return]], CallableProtocol[Params, Return]]:
def decorator(func: CallableProtocol[Params, Return]) -> CallableProtocol[Params, Return]:
assert isinstance(func, protocol)
@wraps(func)
def wrapper(*args: Params.args, **kwargs: Params.kwargs) -> Return:
return func(*args, **kwargs)
return wrapper
return decorator
...んですがまだなんか色々イケてないです。
implements
の使用例
from typing import Protocol, runtime_checkable
@runtime_checkable
class IntTransform(Protocol):
def __call__(self, a: int) -> int: ...
# Argument 1 has incompatible type "Callable[[int], str]"; expected "Callable[[int], int]" [arg-type]mypy(error)
@implements(IntTransform)
def hoge(a: int) -> str:
return str(a)
イケてないポイント
runtime_checkable
なんかダサい
別にこのユースケースに限った話ではないんですが、なんか「runtime_checkable
ついてるとダサいなぁ」というのが拭えないんですよね。どうせならデフォルトでruntime_checkable
でいいのではないか... という気もしますがパフォーマンスに影響がありそうな気もするのであまり深く考えないことにしました。
@implements
のところに出る
エラーが本来hoge
の引数ないし返り値が一致していないので該当する部分にエラーが出てほしいところですが@implements
にデコレートされる関数が引数としておかしいという判定になります。そのため@implements
のところにエラーがあるという表示が出てきてしまいなんともしっくりきません。具体的にどこの部分が間違っているのかはエラーメッセージを読まないと行けず二度手間なのが正直若干面倒です。これでも出ないよりは全然マシですけどね。
エラー文が読みにくい
常々思うことなのですが「got: X, expected Y」系のエラーってものすごく読みにくくないですか。どっちがどっちだっけって毎回なる上に今回のケースのようにCallable
の型パラメータのどこか一箇所だけが違うみたいな場合には間違い探しが大変に厄介です。その点Protocol
の不一致の場合はパラメータの名前まで出してくれるので便利なんですが残念ながら私の理解の範囲ではCallable
を使わざるを得ず読みにくいエラーメッセージのままです。
今後の課題
プラグインを書くのが一番真っ当な未知なんだろうな〜と思いつつも公式がプラグインを書くのをあまり推奨していないのでわざわざ勉強してまで書く気があまり起きていません。そもそも事前に型エラーが発生することをチェックできるようになっただけでかなりの前進なのでとりあえずこれで満足しておこうかなあという気持ちです。もしmypy強者の方でうまいやり方をご存知の方がいらしたらぜひご教示ください。
更新履歴
2023-03-23
概ね問題なく使えていたのですがmypyが変な型を返してくるので修正しました。
Discussion