PythonのCallableで関数に型を付ける
この記事で書くこと
Pythonでは関数というのは重要な役割を果たし、当然だがtype hintでは関数に対して型をつけることができる。
そのためにはCallableという型を使って型アノテーションを行う。
この記事ではCallableを使用して関数に型をつける方法、それにまつわる注意点や限界について書く。
「関数」として扱えるもの
()
付きで呼びされるオブジェクトはPythonにおける関数としてみなせる。
def func():
print("Hello World!")
la = lambda: print("Hello World!")
class Dog:
def __call__(self):
print("Bark bark!")
func() # "Hello World!"
la() # "Hello World!"
d = Dog()
d() # "Bark bark!"
呼び出し可能オブジェクトといっても良い。
この「呼び出し可能」という意味を込めて、関数の型付を行うためのアノテーションオブジェクトをCallableと呼ぶ。
Callableの基本的な使い方
importの方法は2通りあるがどちらでも良い。
from typing import Callable
from collections.abc import Callable
使い方は引数と戻り値の型がジェネリクスになっているので、それらを指定してあげるとよい。
ジェネリクスについては以下を参照。
例えば下の型はintの値を一つ取って、strの値を一つ返すCallable型になる。
Callable[[int], str]
この型を使用すれば引数と戻り値が適合する様々な関数を同一な型として扱える。
def int_multiply_2_to_str(x: int) -> str:
return str(x * 2)
int_to_str: Callable[[int], str] = str
int_to_str(10) # "10"
int_to_str = int_multiply_2_to_str
int_to_str(10) # "20"
int_to_str = lambda x: str(x + 2)
int_to_str(10) # "12"
型に適合しない関数を当てはめると型エラーが起きる。
# intを返す関数はエラー
int_to_str = lambda x: x + 2
# pyright
# error: Expression of type "(x: int) -> int" cannot be assigned to declared type "(int) -> str"
# Type "(x: int) -> int" cannot be assigned to type "(int) -> str"
# Function return type "int" is incompatible with type "str"
# "int" is incompatible with "str" (reportAssignmentType)
# mypy
# error: Incompatible types in assignment (expression has type "Callable[[int], int]", variable has type "Callable[[int], str]") [assignment]
# error: Incompatible return value type (got "int", expected "str") [return-value]
Callableの型の構造
前述の通りCallableはジェネリクスになっており、型を入れ込むためのスロットが2箇所ある。
今、便宜上その2つのスロットをArgs
とReturn
というふうに書く。
Callable[Args, Return]
Argsはpythonのリストを用いて型を記述する。
なぜなら引数は複数ありえるから。
そしてReturnとして一つの型を定める。
戻り値は一つだけなので。
タプルで複数の値を返すときも、それはTuple[int, str]
のように書けるので一つの値の型として記述できる。
例えばstrを一つ取ってintを返す関数の型はこう書ける。
Callable[[str], int]
strとintを取ってintを返す関数はこう。
Callable[[str, int], int]
Callableと型の変性
ジェネリクスとサブタイプの機能をもつ言語では型の変性という機能を持つことが多い。
Pythonも当然型の変性を持つのだが、関数が絡むとこの機能は非常に複雑になる。
詳しくは以下を参照。
Callableの限界とProtocol
Callableには実は限界があって、それらの限界の回避には構造的部分型を実現するためのProtocolを使用すれば回避できる。
引数の名前を指定したいとき
実はCallableでは引数の名前を指定できない。
def print_name(name: str):
print(name)
def print_address(address: str):
print(address)
NamePrinter = Callable[[str], None]
name_printer: NamePrinter = print_name
name_printer = print_address # 名前を縛る方法がないので型チェックが通ってしまう
Protocolを使用すれば名前まで見て型の検査を行える。
class NamePrinterProtocol(Protocol):
def __call__(self, name: str) -> None:
...
name_printer2: NamePrinterProtocol = print_name
name_printer2 = print_address
# pyright
# error: Expression of type "(address: str) -> None" cannot be assigned to declared type "NamePrinterProtocol"
# Type "(address: str) -> None" cannot be assigned to type "(name: str) -> None"
# Parameter name mismatch: "name" versus "address" (reportAssignmentType)
# mypy
# error: Incompatible types in assignment (expression has type "Callable[[str], Any]", variable has type "NamePrinterProtocol") [assignment]
可変長引数を使いたいとき
可変長引数もCallableでは表現できない。
厳密には、任意の長さの任意の型の引数を取る関数は...
(Ellipsis)を使用すれば表現できる。
# 引数は何でもよい
any_func: Callable[..., int] = sum
any_func = lambda : 10
ただし「任意の数のintを引数にする」のようなものは表現できないので、これもProtocolを使う。
class VarArgFunc(Protocol):
def __call__(self, *args: int) -> int:
...
def sum_all(*args: int) -> int:
return sum(args)
def sum_str(*args: str) -> str:
return "".join(args)
var_arg_func: VarArgFunc = sum_all
var_arg_func = sum_str
# pyright
# error: Expression of type "(*args: str) -> str" cannot be assigned to declared type "VarArgFunc"
# Type "(*args: str) -> str" cannot be assigned to type "(*args: int) -> int"
# Parameter 1: type "*tuple[int, ...]" cannot be assigned to type "*tuple[str, ...]"
# "*tuple[int, ...]" is incompatible with "*tuple[str, ...]"
# Tuple entry 1 is incorrect type
# "int" is incompatible with "str"
# Function return type "str" is incompatible with type "int"
# "str" is incompatible with "int" (reportAssignmentType)
# mypy
# error: Incompatible types in assignment (expression has type "Callable[[VarArg(str)], str]", variable has type "VarArgFunc") [assignment]