はじめに
Pythonで関数(Callable
)の型定義(Type Hint)をどうやるのが良いか悩んだのでまとめておきたいと思います。
前提
- Python 3.8以降(確認は3.12で行っています)
- VSCode + Pylance(standard mode) + Error Lens
- mypy 1.8.0
パターン
- 型定義(Type Hint)なし
-
Callable
のみ -
Callable
に引数と返り値の型を付与 - 3.をエイリアスにしたもの
-
Protocol
を使用
これらのパターンで、ソースコード、エディタ上でどうチェックされるか、mypyでどうチェックされるかの結果を見ていきたいと思います。
詳細
1. 型定義(Type Hint)なし
ソースコード
def some_func_1_OK(func, value1, value2):
return func(value1, value2)
def some_func_1_NG(func, value1, value2):
return func(value1, value2, 3)
if __name__ == "__main__":
def add(a: int, b: int) -> int:
return a + b
print(f'add {some_func_1_OK(add, 1, 2)}')
print(f'add {some_func_1_NG(add, 1, 2)}')
エディタ上(Pylanceなどを使用)
some_func_1_NG
は指定できる引数に対して1つ多い引数が指定されていますが、エディタ上では特にチェックされません。
当然、何を指定すればいいかも情報はありません。
mypyの結果
mypyのチェックも成功で通ってしまいます。
> rye run mypy src/python_typed_function/func_no_type.py
Success: no issues found in 1 source file
2. Callable
のみ
次にCallable
だけ指定してみます。
ソースコード
from typing import Callable
def some_func_2_OK(func: Callable, value1, value2) -> int:
return func(value1, value2)
def some_func_2_NG(func: Callable, value1, value2) -> int:
return func(value1, value2, 3)
if __name__ == "__main__":
def add(a: int, b: int) -> int:
return a + b
print(f'add {some_func_2_OK(add, 1, 2)}')
print(f'add {some_func_2_NG(add, 1, 2)}')
エディタ上(Pylanceなどを使用)
同じくNGの方もエディタ上では特にチェックされません。
何らかの関数であることは教えてくれます。
mypyの結果
mypyのチェックも成功で通ってしまいます。
> rye run mypy src/func_callable_only.py
Success: no issues found in 1 source file
3. Callable
に引数と返り値の型を付与
Callableに引数と返り値の型まで指定してみたいと思います。
また、ここからキーワード引数も追加してみたいと思います。
ソースコード
from typing import Callable
def some_func_3_OK(func: Callable[[int, int], int], value1, value2) -> int:
return func(value1, value2)
def some_func_3_OK_2(func: Callable[[int, int], int], value1, value2) -> int:
return func(b=value1, a=value2)
def some_func_3_NG(func: Callable[[int, int], int], value1, value2) -> int:
return func(value1, value2, 3)
def some_func_3_NG_2(func: Callable[[int, int], int], value1, value2) -> int:
return func(b=value1, c=value2)
if __name__ == "__main__":
def add(a: int, b: int) -> int:
return a + b
print(f'add {some_func_3_OK(add, 1, 2)}')
print(f'add {some_func_3_NG(add, 1, 2)}')
print(f'add {some_func_3_OK_2(add, 1, 2)}')
print(f'add {some_func_3_NG_2(add, 1, 2)}')
エディタ上(Pylanceなどを使用)
ここまで来ると、エディタ上でどんな関数を指定すれば良いかまで教えてくれるようになりました。
キーワード引数は、型定義で指定できていない(できない)ので、位置引数がない旨のエラーとして教えてくれています。
また、不要な位置引数もここでチェックしてくれています。
mypyの結果
キーワード引数が指定できないことや、位置引数の数などをここでもチェックしてくれています。
> rye run mypy src/python_typed_function/func_callable_and_type.py
src/python_typed_function/func_callable_and_type.py:7: error: Unexpected keyword argument "b" [call-arg]
src/python_typed_function/func_callable_and_type.py:7: error: Unexpected keyword argument "a" [call-arg]
src/python_typed_function/func_callable_and_type.py:10: error: Too many arguments [call-arg]
src/python_typed_function/func_callable_and_type.py:13: error: Unexpected keyword argument "b" [call-arg]
src/python_typed_function/func_callable_and_type.py:13: error: Unexpected keyword argument "c" [call-arg]
Found 5 errors in 1 file (checked 1 source file)
4. 3.をエイリアスにしたもの
続いてさっきと同じものをエイリアスにしてみたいと思います。
型に名前をつけるようなイメージです。
ソースコード
from typing import Callable
MyCallable = Callable[[int, int], int]
def some_func_4_OK(func: MyCallable, value1, value2) -> int:
return func(value1, value2)
def some_func_4_OK_2(func: MyCallable, value1, value2) -> int:
return func(b=value1, a=value2)
def some_func_4_NG(func: MyCallable, value1, value2) -> int:
return func(value1, value2, 3)
def some_func_4_NG_2(func: MyCallable, value1, value2) -> int:
return func(b=value1, c=value2)
if __name__ == "__main__":
def add(a: int, b: int) -> int:
return a + b
print(f'add {some_func_4_OK(add, 1, 2)}')
print(f'add {some_func_4_NG(add, 1, 2)}')
print(f'add {some_func_4_OK_2(add, 1, 2)}')
print(f'add {some_func_4_NG_2(add, 1, 2)}')
エディタ上(Pylanceなどを使用)
さきほどと同じく、キーワード引数は、型定義で指定できていない(できない)ので、位置引数がない旨のエラーとして教えてくれています。
また、不要な位置引数もここでチェックしてくれています。
mypyの結果
キーワード引数が指定できないことや、位置引数の数などをここでもチェックしてくれています。
> rye run mypy src/python_typed_function/func_callable_and_type_with_alias.py
src/python_typed_function/func_callable_and_type_with_alias.py:9: error: Unexpected keyword argument "b" [call-arg]
src/python_typed_function/func_callable_and_type_with_alias.py:9: error: Unexpected keyword argument "a" [call-arg]
src/python_typed_function/func_callable_and_type_with_alias.py:12: error: Too many arguments [call-arg]
src/python_typed_function/func_callable_and_type_with_alias.py:15: error: Unexpected keyword argument "b" [call-arg]
src/python_typed_function/func_callable_and_type_with_alias.py:15: error: Unexpected keyword argument "c" [call-arg]
Found 5 errors in 1 file (checked 1 source file)
5. Protocol
を使用
最後にプロトコルを使用したパターンです。
ソースコード
from typing import Protocol
class MyCallableType(Protocol):
def __call__(self, a: int, b: int) -> int: # type: ignore
pass
def some_func_5_OK(func: MyCallableType, value1, value2) -> int:
return func(value1, value2)
def some_func_5_OK_2(func: MyCallableType, value1, value2) -> int:
return func(b=value1, a=value2)
def some_func_5_NG(func: MyCallableType, value1, value2) -> int:
return func(value1, value2, 3)
def some_func_5_NG_2(func: MyCallableType, value1, value2) -> int:
return func(b=value1, c=value2)
if __name__ == "__main__":
def add(a: int, b: int) -> int:
return a + b
print(f'add {some_func_5_OK(add, 1, 2)}')
print(f'add {some_func_5_NG(add, 1, 2)}')
print(f'add {some_func_5_OK_2(add, 1, 2)}')
print(f'add {some_func_5_NG_2(add, 1, 2)}')
エディタ上(Pylanceなどを使用)
Protocolを使用するとキーワード引数も定義できるようになりました。
そのため、指定できない名前のキーワード引数のチェックもしてくれています。当然、不要な位置引数もここでチェックしてくれています。
mypyの結果
エディタ上と同じ内容をmypyでもチェックしてくれました。
> rye run mypy src/python_typed_function/func_protocol.py
src/python_typed_function/func_protocol.py:4: note: "__call__" of "MyCallableType" defined here
src/python_typed_function/func_protocol.py:14: error: Too many arguments for "__call__" of "MyCallableType" [call-arg]
src/python_typed_function/func_protocol.py:17: error: Unexpected keyword argument "c" for "__call__" of "MyCallableType" [call-arg]
Found 2 errors in 1 file (checked 1 source file)
まとめ
最後に結果をまとめておきます。
- キーワード引数を含む関数の型定義を行いたい場合は、
Protocol
を使用する - そうではない場合、
Callable
を使用しても良い。複雑な関数になるような場合は特に、エイリアスで型自体に命名するのが良い - 型定義いらない場合は好き選択する
パターン | 型定義 | 型自体への命名 | 位置引数の型定義の可否 | キーワード引数の型定義の可否 |
---|---|---|---|---|
1. 型定義(Type Hint)なし | ✕ | ✕ | ✕ | ✕ |
2. Callable のみ |
△ | ✕ | ✕ | ✕ |
3. Callable に引数と返り値の型を付与 |
◯ | ✕ | ◯ | ✕ |
4. 3.をエイリアスにしたもの | ◯ | ◯ | ◯ | ✕ |
5. Protocol を使用 |
◯ | ◯ | ◯ | ◯ |
個人的にはできるだけ厳密になっている方が好きだったり、制約がしっかりできるの方が好みというのもありますが、こんな結果となりました。
今回は検証のパターンから外しましたが、ABC使って抽象クラスとしてインタフェースを表現するのも良いと思います。