LoginSignup
15
17

More than 3 years have passed since last update.

[Python]変数や引数として扱うCallableオブジェクトに対する型アノテーションの書き方

Posted at

Pythonで関数やメソッドを変数や引数(Callableオブジェクト)として扱う必要が出て来る時があります。

そういった時の型アノテーション周りについて備忘録も兼ねて記事にしておきます。

使う環境

まずは型アノテーションの無い状態

とある以下のような関数があり、引数に関数のオブジェクト(func)と任意の値(x)を受け付けるとします。

def any_function(func, x):
    return func(x)

当たり前ですがこれだとVS Code上でマウスオーバーしてみても型などの情報がUnknownとなってしまいます。Pylance(Pyright)などでも型のチェックなどは行われません。docstringなどで詳細を詳しく書いておくという対策もありますが、このままだと曖昧でミスをしがちです。

image.png

Callableの型アノテーションを行う

続いてfunc引数にCallableの型アノテーションをします。Callableはビルトインのtypingパッケージ内に存在します。ついでにx引数などに対しても型アノテーションをしておきます。

from typing import Callable


def any_function(func: Callable, x: int) -> int:
    return func(x)

こうするとVS Code上でマウスオーバーなどをするとfunc引数は呼び出せるなんらかの関数やメソッドだということを認識してくれます。

また、func引数に関数など以外を指定すればPylanceのチェックで引っかかってくれます。

any_function(func=10, x=20)

image.png

ただしマウスオーバーした際の引数や返却値の情報はUnknownのままです。

image.png

引数や返却情報の型アノテーションを行う

func関数を第一引数にint、第二引数にfloat、返却値にfloatを返すといった型アノテーションをしていきます(サンプルの引数にも、yというflaotの引数を追加していきます)。

書き方としてはCallable[[第一引数の型, 第二引数の型], 返却値の型]といったように書きます。引数が増減した場合にはコンマ区切りで増やしたり減らしたりします。

def any_function(
        func: Callable[[int, float], float],
        x: int,
        y: float) -> float:
    return func(x, y)

これによって、マウスオーバー時に関数の引数や返却値の型情報が表示されたり、func引数を呼び出した際に引数が足りなかったり型が一致していなければエラーで検知できるようになります。

image.png

func呼び出し箇所で引数が足りていないサンプル
def any_function(
        func: Callable[[int, float], float],
        x: int,
        y: float) -> float:
    return func(x)

image.png

func呼び出しで型が一致していないサンプル
def any_function(
        func: Callable[[int, float], float],
        x: int,
        y: float) -> float:
    return func(y, x)

image.png

引数に渡す時などにも指定した関数の構造が一致しているかがチェックされます。

引数に渡しているCallableの返却値が一致しておらずチェックに引っかかるサンプル
def any_function(
        func: Callable[[int, float], float],
        x: int,
        y: float) -> float:
    return func(x, y)


def other_function(x: int, y: float) -> str:
    ...


any_function(func=other_function, x=10, y=12.5)

image.png

ここまでの対応でも型アノテーションの無いケースと比べると大分堅牢な感じになってきました。

ただしこの場合キーワード引数の指定が元の引数名が使えません。引数名は保持されず、p0, p1, ...という名前が割り振られます。例えば以下のようにキーワード引数付きで書くとPylanceのチェックで引っかかってしまいます。

def any_function(
        func: Callable[[int, float], float],
        x: int,
        y: float) -> float:
    return func(x=x, y=y)

image.png

多くのケースではこれでも特に問題にならない場合も多いですが、例えば引数が多かったり同じ型の引数がたくさんあるとキーワード引数を使いたくなってきます。

引数名の情報を保持する形で型アノテーションを行う

実はtypingパッケージのProtocolを使うと、引数名も保持した状態で型アノテーションをすることができます(最近知りました)。

使い方としては、Protocolを継承した形でクラスを定義し、__call__メソッドに必要な引数や返却値・型の情報などを定義します(今回はサンプルとしてFuncTypeという名前にしました)。あとはそのクラス(FuncType)をfunc引数の型アノテーションに指定すればOKです。

from typing import Callable, Protocol


class FuncType(Protocol):

    def __call__(self, x: int, y: float) -> float:
        ...


def any_function(
        func: FuncType,
        x: int,
        y: float) -> float:
    return func(x=x, y=y)

これでキーワード引数を使ってもPylance上でエラーが出なくなります。

image.png

ProtocolはPython3.8以降で利用ができます。Python3.7以前のバージョンで使いたい場合や配布ライブラリなどで過去のPythonバージョンもサポートしたい場合にはバックポート的にpipなどでtyping-extensionsライブラリをインストールする必要があります。mypyなどをインストールした場合には一緒にインストールされます。

importの記述もtypingモジュールではなくtyping_extensionsというように記述が変わります。挙動や使い方はtypingのProtocolもtyping_extensionsのProtocolも変わりません。

from typing_extensions import Protocol

参考サイト・参考文献まとめ

15
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
17