コンストラクタ

コンストラクタの呼び出しは、型チェッカー内で特別な処理が必要です。

コンストラクタの呼び出し

実行時にクラスのコンストラクタを呼び出すと、通常、次の順序で 3 つのメソッドが呼び出されます。

  1. メタクラスの __call__ メソッド(通常は type クラスによって提供されますが、カスタムメタクラスによってオーバーライドされることがあり、次の 2 つのメソッドを呼び出す責任があります)

  2. クラスの __new__ 静的メソッド

  3. クラスの __init__ インスタンスメソッド

型チェッカーは、コンストラクタ呼び出しを分析する際に、この実行時の動作を反映する必要があります。

メタクラス __call__ メソッド

コンストラクタ呼び出しを評価する際に、型チェッカーは最初にクラスに __call__ メソッドを定義するカスタムメタクラス(type のサブクラス)があるかどうかを確認する必要があります。 もしそうであれば、提供された引数を使用してこのメソッドの呼び出しを評価する必要があります。 メタクラスが type である場合、このステップはスキップできます。

__call__ メソッドの評価された戻り値の型が、構築されているクラスのインスタンス以外のものであることを示している場合、型チェッカーはメタクラス __call__ メソッドが特別な方法で type.__call__ をオーバーライドしていると仮定し、クラスの __new__ または __init__ メソッドを評価しないようにする必要があります。 たとえば、いくつかのメタクラス __call__ メソッドは、コンストラクタ呼び出しがそのクラスではサポートされていないことを示すために NoReturn を返すように注釈されています。

class Meta(type):
    def __call__(cls, *args, **kwargs) -> NoReturn:
        raise TypeError("Cannot instantiate class")

class MyClass(metaclass=Meta):
    def __new__(cls, *args, **kwargs) -> Self:
        return super().__new__(cls, *args, **kwargs)

assert_type(MyClass(), Never)

__call__ に戻り値の型注釈が提供されていない場合、型チェッカーはそれが特別な方法で type.__call__ をオーバーライドしていないと仮定し、戻り値の型が cls パラメータで指定された型のインスタンスであるかのように進めることができます。

__new__ メソッド

メタクラス __call__ メソッドが評価された後、型チェッカーはクラスの __new__ メソッド(該当する場合)を提供された引数を使用して評価する必要があります。 クラスが __new__ メソッドを定義しておらず、object 以外の基底クラスから __new__ メソッドを継承していない場合、このステップはスキップする必要があります。

クラスがジェネリックで明示的に特殊化されている場合、型チェッカーは提供された型引数を使用して __new__ メソッドを部分的に特殊化する必要があります。 クラスが明示的に特殊化されていない場合、クラススコープの型変数はコンストラクタ呼び出しに渡された提供された引数を使用して解決する必要があります。

class MyClass[T]:
    def __new__(cls, x: T) -> Self:
        return super().__new__(cls)

# 特殊化されたクラスのコンストラクタ呼び出し
assert_type(MyClass[int](1), MyClass[int])
assert_type(MyClass[float](1), MyClass[float])
MyClass[int](1.0)  # 型エラー

# 非特殊化クラスのコンストラクタ呼び出し
assert_type(MyClass(1), MyClass[int])
assert_type(MyClass(1.0), MyClass[float])

提供された引数を使用して __new__ メソッド呼び出しを評価する際に、クラススコープの型変数が解決されない場合、これらの型変数は解決されないままにしておき、__init__ メソッド(該当する場合)を使用してそれらを解決できるようにする必要があります。

class MyClass[T]:
    def __new__(cls, *args, **kwargs) -> Self:
        return super().__new__(cls)

    def __init__(self, x: T) -> None:
        pass

assert_type(MyClass(1), MyClass[int])
assert_type(MyClass(""), MyClass[str])

ほとんどのクラスでは、__new__ メソッドの戻り値の型は通常 Self ですが、他の型も許可されます。 たとえば、__new__ メソッドはサブクラスのインスタンスや完全に無関係なクラスのインスタンスを返すことができます。

__new__ の評価された戻り値の型が構築されているクラス(またはそのサブクラス)でない場合、型チェッカーは __init__ メソッドが呼び出されないと仮定する必要があります。 これは type.__call__ メソッドの実行時の動作と一致しています。 __new__ メソッドの戻り値の型が構築されているクラス(またはそのサブクラス)でないメンバーを含む共用体である場合、型チェッカーは同様に __init__ メソッドが呼び出されないと仮定する必要があります。

class MyClass:
    def __new__(cls) -> int:
        return 0

    # この場合、型チェッカーはコンストラクタ呼び出しを評価する際に __init__ メソッドを考慮しない必要があります。
    def __init__(self, x: int):
        pass

assert_type(MyClass(), int)

このテストの目的のために、Any``(または ``Any を含む共用体)の明示的な戻り値の型は、構築されているクラスのインスタンスでない型として扱う必要があります。

class MyClass:
    def __new__(cls) -> Any:
        return 0

    # この場合、__init__ メソッドは呼び出されないため、評価されるべきではありません。
    def __init__(self, x: int):
        pass

assert_type(MyClass(), Any)

__new__ の戻り値の型が注釈されていない場合、型チェッカーは戻り値の型が Self であると仮定し、__init__ メソッドが呼び出されると仮定して進めることができます。

クラスがジェネリックである場合、__new__ メソッドは特殊化されたクラス型をオーバーライドし、異なる型引数で特殊化されたクラスインスタンスを返すことができます。

class MyClass[T]:
    def __new__(cls, *args, **kwargs) -> "MyClass[list[T]]":
        ...

assert_type(MyClass[int](), MyClass[list[int]])

__new__ メソッド内の cls パラメータが注釈されていない場合、型チェッカーは type[Self] の型を推論する必要があります。 cls パラメータの型が明示的であるか推論されるかに関係なく、型チェッカーは構築されているクラスを cls パラメータにバインドし、バインド中に発生する型エラーを報告する必要があります。

class MyClass[T]:
    def __new__(cls: "type[MyClass[int]]") -> "MyClass[int]": ...

MyClass()  # OK
MyClass[int]()  # OK
MyClass[str]()  # 型エラー

__init__ メソッド

__new__ メソッドを評価した後、型チェッカーは __init__ メソッド(該当する場合)を提供された引数を使用して評価する必要があります。 クラスがジェネリックで明示的に特殊化されている場合(または __new__ メソッドの戻り値の型を介して特殊化されている場合)、型チェッカーは提供された型引数を使用して __init__ メソッドを部分的に特殊化する必要があります。 クラスが明示的に特殊化されていない場合、クラススコープの型変数はコンストラクタ呼び出しに渡された提供された引数を使用して解決する必要があります。

クラスが __init__ メソッドを定義しておらず、object 以外の基底クラスから __init__ メソッドを継承していない場合、このステップはスキップする必要があります。

class MyClass[T]:
    def __init__(self, x: T) -> None:
        ...

# 特殊化されたクラスのコンストラクタ呼び出し
assert_type(MyClass[int](1), MyClass[int])
assert_type(MyClass[float](1), MyClass[float])
MyClass[int](1.0)  # 型エラー

# 非特殊化クラスのコンストラクタ呼び出し
assert_type(MyClass(1), MyClass[int])
assert_type(MyClass(1.0), MyClass[float])

__init__ メソッド内の self パラメータが注釈されていない場合、型チェッカーは Self の型を推論する必要があります。 self パラメータの型が明示的であるか推論されるかに関係なく、型チェッカーは構築されているクラスをこのパラメータにバインドし、バインド中に発生する型エラーを報告する必要があります。

class MyClass[T]:
    def __init__(self: "MyClass[int]") -> None: ...

MyClass()  # OK
MyClass[int]()  # OK
MyClass[str]()  # 型エラー

__init__ の戻り値の型は常に None であり、これはメソッドが戻り値の型を指定することによってコンストラクタ呼び出しの戻り値の型に影響を与えることができないことを意味します。 __init__ メソッドが戻り値の型に影響を与えることが望ましい場合があります。特に、__init__ メソッドがオーバーロードされている場合です。 これを可能にするために、型チェッカーは self パラメータを注釈して、コンストラクタ呼び出しの結果の型に影響を与える型を指定できるようにする必要があります。

class MyClass1[T]:
    @overload
    def __init__(self: "MyClass1[list[int]]", value: int) -> None: ...
    @overload
    def __init__(self: "MyClass1[set[str]]", value: str) -> None: ...
    @overload
    def __init__(self, value: T) -> None: ...


assert_type(MyClass1(0), MyClass1[list[int]])
assert_type(MyClass1[int](3), MyClass1[int])
assert_type(MyClass1(""), MyClass1[set[str]])
assert_type(MyClass1(3.0), MyClass1[float])

関数スコープの型変数も __init__ メソッドの self 注釈に使用して、コンストラクタ呼び出しの戻り値の型に影響を与えることができます。

class MyClass2[T1, T2]:
    def __init__[V1, V2](self: "MyClass2[V1, V2]", value1: V1, value2: V2) -> None: ...

assert_type(MyClass2(0, ""), MyClass2[int, str])
assert_type(MyClass2[int, str](0, ""), MyClass2[int, str])

class MyClass3[T1, T2]:
    def __init__[V1, V2](self: "MyClass3[V2, V1]", value1: V1, value2: V2) -> None: ...

assert_type(MyClass3(0, ""), MyClass3[str, int])
assert_type(MyClass3[str, int](0, ""), MyClass3[str, int])

クラススコープの型変数は self 注釈に使用すべきではありません。なぜなら、そのような使用は曖昧または意味のない型評価結果をもたらす可能性があるからです。 型チェッカーは、__init__ メソッドの self パラメータの型注釈内でクラススコープの型変数が使用されている場合、エラーを報告する必要があります。

class MyClass4[T1, T2]:
    # ``self`` 注釈は型エラーを引き起こすべきです
    def __init__(self: "MyClass4[T2, T1]") -> None: ...

__new__ および __init__ メソッドのないクラス

クラスが __new__ メソッドまたは __init__ メソッドを定義しておらず、object 以外の基底クラスからこれらのメソッドを継承していない場合、型チェッカーは object クラスの __new__ および __init__ メソッドを使用して引数リストを評価する必要があります。

class MyClass5:
    pass

MyClass5()  # OK
MyClass5(1)  # 型エラー

type[T] のコンストラクタ呼び出し

type[T] 型の値(T が具体的なクラスまたは型変数である場合)を呼び出すと、型チェッカーはクラス T``(または型変数 ``T の上限を表すクラス)で呼び出しが行われているかのようにコンストラクタ呼び出しを評価する必要があります。 これは、型チェッカーが T のメタクラスの __call__ メソッドおよび T__new__ および __init__ メソッドを使用してコンストラクタ呼び出しを評価する必要があることを意味します。

このようなコードは安全でない可能性があることに注意する必要があります。なぜなら、type[T] の型は T のサブクラスを表す可能性があり、それらのサブクラスは基底クラスと互換性のない方法で __new__ および __init__ メソッドを再定義する可能性があるからです。 同様に、T のメタクラスは、基底メタクラスと互換性のない方法で __call__ メソッドを再定義する可能性があります。

構築中の特殊化

前述のように、クラスがジェネリックで明示的に特殊化されていない場合、その型変数は __new__ および __init__ メソッドに渡された引数を使用して解決する必要があります。 1 つ以上の型変数がこれらのメソッド評価中に解決されない場合、それらはデフォルト値を取る必要があります。

T1 = TypeVar("T1")
T2 = TypeVar("T2")
T3 = TypeVar("T3", default=str)

class MyClass1(Generic[T1, T2]):
    def __new__(cls, x: T1) -> Self: ...

assert_type(MyClass1(1), MyClass1[int, Any])

class MyClass2(Generic[T1, T3]):
    def __new__(cls, x: T1) -> Self: ...

assert_type(MyClass2(1), MyClass2[int, str])

__new__ および __init__ の一貫性

型チェッカーはオプションで、クラスの __new__ および __init__ メソッドが consistent なシグネチャを持っていることを検証できます。

class MyClass:
    def __new__(cls) -> Self:
        return super().__new__(cls)

    # 型エラー: __new__ と __init__ のシグネチャが一貫していません
    def __init__(self, x: str) -> None:
        pass

コンストラクタを Callable に変換する

クラスオブジェクトは呼び出し可能であり、これはクラスオブジェクトの型が呼び出し可能な型に assignable であることを意味します。

def accepts_callable[**P, R](cb: Callable[P, R]) -> Callable[P, R]:
    return cb

class MyClass:
    def __init__(self, x: int) -> None:
        pass

reveal_type(accepts_callable(MyClass))  # ``def (x: int) -> MyClass``

クラスを呼び出し可能な型に変換する際に、型チェッカーは次のルールを使用する必要があります。これらのルールは、コンストラクタ呼び出しを評価するために上記で指定されたルールと同じです。

  1. クラスがカスタムメタクラスを持ち、そのメタクラスが構築されているクラスのサブクラス以外の型(またはそのような型を含む共用体)を戻り値として注釈された __call__ メソッドを定義している場合、型チェッカーはメタクラス __call__ メソッドが特別な方法で type.__call__ をオーバーライドしていると仮定する必要があります。 この場合、呼び出し可能な型は、クラスにバインドされた後のメタクラス __call__ メソッドのパラメータと戻り値から合成される必要があり、__new__ または __init__ メソッド(存在する場合)は無視される必要があります。 これはまれなケースです。 より一般的なケースでは、特別な方法で type.__call__ をオーバーライドするカスタムメタクラスが存在しない場合、呼び出し可能な型に変換する目的でメタクラス __call__ シグネチャは無視されるべきです。 カスタムメタクラス __call__ メソッドが存在するが、注釈された戻り値の型がない場合、型チェッカーはメソッドが type.__call__ のように動作すると仮定し、次のステップに進むことができます。

  2. クラスが __new__ メソッドを定義しているか、object 以外の基底クラスから __new__ メソッドを継承している場合、型チェッカーはクラスにバインドされた後のそのメソッドのパラメータと戻り値から呼び出し可能な型を合成する必要があります。

  3. ステップ 2 のメソッドの戻り値の型が構築されているクラスのサブクラス(またはそのようなクラスを含む共用体)でない型に評価される場合、最終的な呼び出し可能な型はステップ 2 の結果に基づき、変換プロセスは完了します。 この場合、__init__ メソッドは無視されます。 これは type.__call__ メソッドの実行時の動作と一致しています。

  4. クラスが __init__ メソッドを定義しているか、object 以外の基底クラスから __init__ メソッドを継承している場合、呼び出し可能な型は、ステップ 2 の結果として得られたクラスインスタンスにバインドされた後の __init__ メソッドのパラメータから合成される必要があります。 この合成された呼び出し可能な型の戻り値は、Self の具体的な値である必要があります。

  5. ステップ 2 および 4 の両方が結果を生成しない場合、クラスが object 以外のクラスから __new__ または __init__ メソッドを定義または継承していないため、型チェッカーは object クラスの __new__ および __init__ メソッドから呼び出し可能な型を合成する必要があります。

  6. ステップ 2、4、および 5 は、1 つまたは 2 つの呼び出し可能な型を生成します。 変換プロセスの最終結果は、これらの型の共用体です。 これは、適用可能な __new__ および __init__ メソッドの呼び出し可能なシグネチャを反映します。

class A:
    """ No __new__ or __init__ """
    pass

class B:
    """ __new__ and __init__ """
    def __new__(cls, *args, **kwargs) -> Self:
        ...

    def __init__(self, x: int) -> None:
        ...

class C:
    """ __new__ but no __init__ """
    def __new__(cls, x: int) -> int:
        ...

class CustomMeta(type):
    def __call__(cls) -> NoReturn:
        raise NotImplementedError("Class not constructable")

class D(metaclass=CustomMeta):
    """ Custom metaclass that overrides type.__call__ """
    def __new__(cls, *args, **kwargs) -> Self:
        """ This __new__ is ignored for purposes of conversion """
        pass


class E:
    """ __new__ that causes __init__ to be ignored """

    def __new__(cls) -> A:
        return A.__new__(cls)

    def __init__(self, x: int) -> None:
        """ This __init__ is ignored for purposes of conversion """
        ...


reveal_type(accepts_callable(A))  # ``def () -> A``
reveal_type(accepts_callable(B))  # ``def (*args, **kwargs) -> B | def (x: int) -> B``
reveal_type(accepts_callable(C))  # ``def (x: int) -> int``
reveal_type(accepts_callable(D))  # ``def () -> NoReturn``
reveal_type(accepts_callable(E))  # ``def () -> A``

__init__ または __new__ メソッドがオーバーロードされている場合、呼び出し可能な型はオーバーロードから合成される必要があります。 結果の呼び出し可能な型自体もオーバーロードされます。

class MyClass:
    @overload
    def __init__(self, x: int) -> None: ...
    @overload
    def __init__(self, x: str) -> None: ...

reveal_type(accepts_callable(MyClass))  # overload of ``def (x: int) -> MyClass`` and ``def (x: str) -> MyClass``

クラスがジェネリックである場合、合成された呼び出し可能な型にはシグネチャ内に現れるクラススコープの型パラメータが含まれる必要がありますが、これらの型パラメータは呼び出し可能な型の関数スコープの型パラメータに変換される必要があります。 __init__ または __new__ メソッド内の関数スコープの型パラメータも、合成された呼び出し可能な型の関数スコープの型パラメータとして含まれる必要があります。

class MyClass[T]:
    def __init__[V](self, x: T, y: list[V], z: V) -> None: ...

reveal_type(accepts_callable(MyClass))  # ``def [T, V] (x: T, y: list[V], z: V) -> MyClass[T]``