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 ということになっているがこれはプロトコル言語機能の実装前の言い分であり、実際には継承せずとも動作するためプロトコルとみなすべきだろう。 ↩