float(x) できるなら SupportsFloat
Python には SupportsFloat というプロトコル1がある。
これは、Python 組み込みの float 型ではないが、float として扱うことができる型であることを要求する。実際の動作としては、self.__float__() をメンバーに持つ、つまり組み込み関数 float(x) で変換できることを意味する。float ではないが SupportsFloat であるような型には NumPy の np.float64 などがある。
from typing import SupportsFloat
import numpy as np
def func1(x: float) -> float:
return 2.0 * float(x)
def func2(x: SupportsFloat) -> float:
return 2.0 * float(x)
x = np.empty((8, 8), dtype=float)
y: np.float64 = x[0, 0]
print(func1(y)) # NG
print(func2(y)) # OK
complex(x) できるなら SupportsComplex、ではない
同様に self.__complex__() を実装していることを意味する SupportsComplex がある。float 型は complex(x) によって complex に変換できるため、以下のコードは型チェッカーを通過できるはずだ。
from typing import SupportsComplex
def distance(x: SupportsComplex, y: SupportsComplex) -> float:
return abs(complex(x) - complex(y))
print(distance(5.0, 3.0)) # NG
しかし、このコードは型チェッカーが許さない。なぜだろう?
実は、float は self.__complex__() を持っていないのだ。つまり float は SupportsComplex ではない。
print(hasattr(5.0, "__complex__")) # False
さらにいえば、complex さえ Python 3.11 まで SupportsComplex ではなかった。SupportsComplex ってなんだよ。
ではなぜ complex(x) によって変換できるのか? complex 関数(コンストラクタ)の型シグネチャは Pyright によるとcomplex(real: complex | SupportsComplex | SupportsFloat | SupportsIndex = ..., imag: complex | SupportsFloat | SupportsIndex = ...) となっている。SupportsComplex だけでなく SupportsFloat や SupportsIndex が指定されていることがわかるだろう。SupportsIndex は operator.index(x) によって正確な int に変換できる、つまり整数とみなせる型であることを意味する。
もしや、float や int に対して特殊化し、float(x) や operator.index(x) によって変換できる型はそのようにしてから処理をおこなっているのではないか? 確かめてみよう。
class Foobar:
def __float__(self) -> float:
print("called __float__")
return 0.0
complex(Foobar()) # called __float__
間違いない。つまり、complex(x) によって変換できることを要求する場合は、例えば以下のようにしなければならない。
from typing import SupportsComplex, SupportsFloat, SupportsIndex, TypeAlias
ConvertibleToComplex: TypeAlias = (
complex | SupportsComplex | SupportsFloat | SupportsIndex
)
def distance(x: ConvertibleToComplex, y: ConvertibleToComplex) -> float:
return abs(complex(x) - complex(y))
print(distance(5.0, 3.0)) # OK
確かに float に変換できるなら complex にも変換できるべきなので、self.__float__() だけ実装すればいい complex(x) の型は理解できる。しかし、SupportsComplex というプロトコルの動作が非直感的になっていることは念頭に置かなければいけない。
おまけ
では、self.__index__() self.__float__() self.__complex__() すべてを実装している場合 complex(x) の動作はどうなるのだろう。試してみよう。
class Foobar:
def __index__(self) -> int:
print("called __index__")
return 0
def __float__(self) -> float:
print("called __float__")
return 0.0
def __complex__(self) -> complex:
print("called __complex__")
return complex()
complex(Foobar()) # called __complex__
self.__complex__()、self.__float__()、self.__index__() の順で優先されて呼び出されるようだ。
-
ABC ということになっているがこれはプロトコル言語機能の実装前の言い分であり、実際には継承せずとも動作するためプロトコルとみなすべきだろう。 ↩