パラメータ仕様変数により関数の引数を参照した型定義が可能に
パラメータ仕様変数とは、引数仕様変数ともいい、関数の引数の仕様を収納した変数のことをいいます。デコレータを使った関数呼び出しの型チェックを厳密化するときなどに使用しますが、これだけではわかりにくいと思うので、まずはデコレータ関数のサンプルを紹介し、そこで起きる問題を見ながら理解していきましょう。
デコレータ関数とは?
デコレータとは、クラスや関数の前後に実行する処理を追加できる機能です。デコレータ関数は、その処理内容を記述した関数であり、その関数をクラスや関数の定義の直前に「@関数名」と記述することで呼び出すことができます。以下のリストは、デコレータの定義例です。
from typing import Callable, TypeVar # 戻り値の型を表す型変数の定義 R = TypeVar("R") (1) # デコレータ関数loggingの定義 def logging(f: Callable[..., R]) -> Callable[..., R]: (2) def wrapper(*args: object, **kwargs: object) -> R: (3) print(f'{f.__name__}の実行前') val = f(*args, **kwargs) print(f'{f.__name__}の実行後') return val return wrapper (4) # デコレータを使う関数の定義 @logging def simple_function(x: str, y: int) -> int: print(f'{x}, {y}') return int(x) + y # デコレータ関数の呼び出し simple_function("200", 100) (5)
(1)は型変数Rを定義しています。これはデコレータ関数の引数と戻り値に指定されるCallableオブジェクトの戻り値の型を指定するための型ヒントになります。
(2)の関数loggingは、簡易なロガーであり、デコレート対象の関数の呼び出しに伴い、呼び出されます。引数と戻り値はCallableオブジェクトであり、これはすなわちデコレート対象の関数とデコレート後の関数そのものとなります。ここで指定するCallableオブジェクトは、引数は任意(...はEllipsisオブジェクトで、省略を意味します)、戻り値はRとなっていることに注意してください。
(3)で定義している関数wrapperは、デコレータ関数loggingが返す関数となります。wrapper関数には、デコレート対象の関数の引数が渡されます。この引数を用いて、logging関数に渡された関数を呼び出します。関数の呼び出し前後には、print文で実行前後のメッセージを出力しています。
(4)のreturn文はlogging関数の戻り値であり、(3)で定義した関数です。
(5)の関数呼び出しでは、デコレータ関数loggingが「simple_function("200", 100)」を引数として呼び出され、ラップされた関数が返されることで、その関数が実行されます。
実行結果は、以下のようになります。本来のsimple_function関数による出力が、デコレータ関数による出力に挟まれていることが分かります。
simple_functionの実行前 200, 100 simple_functionの実行後
なお、(5)を以下のように書き換えて実行すると、関数の呼び出しがエラーとなります。それは、第1引数が整数であるので、int(x)がエラーとなるためです。しかし、mypyによる静的な型チェックでは検出できません。それは、(5)の関数呼び出しは実際にはデコレータによって返された関数の呼び出しであり、その関数である(2)のCallableオブジェクトの引数は、何でもよい(...)ということになっているからです。
simple_function(200, "100") # 実行時エラー
パラメータ仕様変数の追加
このような問題を受けて、Python 3.10ではパラメータ仕様変数(ParamSpec)が導入されました。上記のリストをパラメータ仕様変数を使って書き換えたのが以下のリストです。
from typing import Callable, TypeVar, ParamSpec (1) # 戻り値の型変数とパラメータ仕様変数の定義 R = TypeVar("R") P = ParamSpec("P") (2) # デコレータ関数loggingの定義 def logging(f: Callable[P, R]) -> Callable[P, R]: (3) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: (4) print(f'{f.__name__}の実行前') val = f(*args, **kwargs) print(f'{f.__name__}の実行後') return val return wrapper …略… # デコレータ関数の呼び出し simple_function("200", 100) simple_function(200, "100") (5)
(1)でParamSpecを参照できるようにimport文を拡張しています。
(2)で定義しているPが、パラメータ仕様変数です。TypeVarと同様にPsramSpecを使って定義します。
(3)ではlogging関数を定義していますが、引数のCallableオブジェクトの引数がパラメータ仕様変数Pとなっていることに注目です。これにより、Pを使った型ヒントが可能になっています。
(4)のwrapper関数も、引数の型ヒントがP.args、P.kwargsと、それぞれパラメータ仕様変数から得られる型ヒントになることにも注目です。
このリストをmypyに与えてみると、以下のような結果になります。引数に互換性のない(5)の呼び出しでは、arg-typeのエラーになることが分かります。
% mypy paramspec.py paramspec.py:24: error: Argument 1 to "simple_function" has incompatible type "int"; expected "str" [arg-type] paramspec.py:24: error: Argument 2 to "simple_function" has incompatible type "str"; expected "int" [arg-type] Found 2 errors in 1 file (checked 1 source file)
このようにデコレータを使う関数の引数に対する型ヒントを厳密化でき、実行するまでもなく静的型チェックで型エラーを検出することができるようになります。