Python 3.11の型ヒントに導入される"PEP 673 – Self Type"を訳してみた
この記事は
Python 3.11の型ヒント関連の新機能として、PEP 673 – Self Typeが採択されていたので、読み解いてみます。
13k字超の大作になってしまった...
長いので先に感想
- 冗長だった型表現が簡潔に書けてよさそう
- Adaptor / Factoryパターンの設計をする時に型がつけられそう
- Pyramid, Zope / Ploneの型付けに期待...?
- まだ抽象クラス・メタクラスを使う時はしばしば冗長
- これは意図して冗長にしている認識。簡潔に書こうとすると混乱が始まる
以下拙訳を掲載。訳の下に個人的所感を書いています
要約
This PEP introduces a simple and intuitive way to annotate methods that return an instance of their class. This behaves the same as the TypeVar-based approach specified in PEP 484 but is more concise and easier to follow.
自分自身を返すメソッドに型をつけられるようになる。その振る舞いはTypeVar
ベースのアプローチと同じだけれど、より明確で型を追いやすくなっている。
どう変わるのか?
今までだと
A common use case is to write a method that returns an instance of the same class, usually by returning self.
class Shape:
def set_scale(self, scale: float):
self.scale = scale
return self
Shape().set_scale(0.5) # => Shapeに推論される
まぁこれは普通に推論される。これだけだと利点がわからないので続きに
class Shape:
def set_scale(self, scale: float) -> Shape:
self.scale = scale
return self
Shape().set_scale(0.5) # => Shape
これも明示的に書いているのでそうなる。では次はどうなのか?
サブクラスが継承元のメソッドを使っていて、その先で継承先のメソッドをチェーンする時
class Circle(Shape):
def set_radius(self, r: float) -> Circle:
self.radius = r
return self
Circle().set_scale(0.5) # *Shape*であり, Circleではない
Circle().set_scale(0.5).set_radius(2.7)
# => error: Shape has no attribute "set_radius"
エラーになってしまう。
Workaroundはあるけれど、冗長
typing.TypeVar
を使えば回避できるが、ちょっと冗長
from typing import TypeVar
TShape = TypeVar("TShape", bound="Shape")
class Shape:
def set_scale(self: TShape, scale: float) -> TShape:
self.scale = scale
return self
class Circle(Shape):
def set_radius(self, radius: float) -> Circle:
self.radius = radius
return self
Circle().set_scale(0.5).set_radius(2.7) # => Circle
なので、Python 3.11ではこうやって書けるようになる
typing.Self
を使うことによって自分自身を動的に定義できるようになる
from typing import Self
class Shape:
def set_scale(self, scale: float) -> Self:
self.scale = scale
return self
class Circle(Shape):
def set_radius(self, radius: float) -> Self:
self.radius = radius
return self
Circle().set_scale(0.5) # => Circleに推論される!
その他の仕様
classmethodでは...
今まで通りclassmethodを書くと、
class Shape:
def __init__(self, scale: float) -> None: ...
@classmethod
def from_config(cls, config: dict[str, float]) -> Shape:
return cls(config["scale"])
ただしこれは継承先のメソッドを呼んでも全部Shape
に推論されてしまうので、問題がある
class Circle(Shape): ...
def circumference
shape = Shape.from_config({"scale": 7.0})
# => Shape
circle = Circle.from_config({"scale": 7.0})
# => Circleではなく *Shape* になってしまうので...
circle.circumference()
# error: `Shape` has no attribute `circumference`
先程と同様にworkaroundもあるけれど、やはり冗長
Self = TypeVar("Self", bound="Shape")
class Shape:
@classmethod
def from_config(
cls: type[Self], config: dict[str, float]
) -> Self:
return cls(config["scale"])
なので、こんな感じで書けるようになる
from typing import Self
class Shape:
@classmethod
def from_config(cls, config: dict[str, float]) -> Self:
return cls(config["scale"])
引数に自分自身を取る時
他の用途として、メソッドの引数に自分自身を取る時、
Self = TypeVar("Self", bound="Shape")
class Shape:
def difference(self: Self, other: Self) -> float: ...
def apply(self: Self, f: Callable[[Self], None]) -> None: ...
と書けるが、冗長なので(ry
from typing import Self
class Shape:
def difference(self, other: Self) -> float: ...
def apply(self, f: Callable[[Self], None]) -> None: ...
スッキリ書けるようになる。もちろんself
はSelf
型、つまりShape
を指す
言うまでもなく、selfを明示的に書くことも可能
class Shape:
def difference(self: Self, other: Self) -> float: ...
Attributeに型をつける時は
例えばLinkedList
はAttributeが現在のclassのsub-classでなければならないとき、
from dataclasses import dataclass
from typing import Generic, TypeVar
T = TypeVar("T")
@dataclass
class LinkedList(Generic[T]):
value: T
next: LinkedList[T] | None = None
# OK
LinkedList[int](value=1, next=LinkedList[int](value=2))
# だめ
LinkedList[int](value=1, next=LinkedList[str](value="hello"))
ただし、next
Attributeに型LinkedList[T]
を付与してしまうと無効な構造ができてしまう
from dataclasses import dataclass
from typing import Generic, TypeVar
T = TypeVar("T")
@dataclass
class LinkedList(Generic[T]):
value: T
next: LinkedList[T] | None = None
# OK
LinkedList[int](value=1, next=LinkedList[int](value=2))
# だめだけど通ってしまう
LinkedList[int](value=1, next=LinkedList[str](value="hello"))
そこで、next: Self | None
(訳注: typing.Optionalを用いても良さそう)を使うと、このように表現できる
from typing import Self
@dataclass
class LinkedList(Generic[T]):
value: T
next: Self | None = None
@dataclass
class OrdinalLinkedList(LinkedList[int]):
def ordinal_value(self) -> str:
return as_ordinal(self.value)
xs = OrdinalLinkedList(value=1, next=LinkedList[int](value=2))
# TypeError: Expected OrdinalLinkedList, got LinkedList[int].
if xs.next is not None:
xs.next = OrdinalLinkedList(value=3, next=None) # OK
xs.next = LinkedList[int](value=3, next=None) # だめ
これは、Self
型を含む各Attributeをその型を返すPropertyとして扱うのと同等で、typing.TypeVar
を使ってこのようにも書ける
from dataclasses import dataclass
from typing import Any, Generic, TypeVar
T = TypeVar("T")
Self = TypeVar("Self", bound="LinkedList")
class LinkedList(Generic[T]):
value: T
@property
def next(self: Self) -> Self | None:
return self._next
@next.setter
def next(self: Self, next: Self | None) -> None:
self._next = next
class OrdinalLinkedList(LinkedList[int]):
def ordinal_value(self) -> str:
return str(self.value)
Generic Classでは
Self
はGenericなclassのメソッドでも使えるので、
class Container(Generic[T]):
value: T
def set_value(self, value: T) -> Self: ...
と書くことができて、typing.TypeVar
を使って(ry
Self = TypeVar("Self", bound="Container[Any]")
class Container(Generic[T]):
value: T
def set_value(self: Self, value: T) -> Self: ...
振る舞いはメソッドが呼び出されたObjectの型引数を保持するので、具象型をのメソッドContainer[int]
呼び出すとSelf
はContainer[int]
になるし、ジェネリック型Container[T]
のメソッドを呼び出すともちろんContainer[T]
となる。
def object_with_concrete_type() -> None:
int_container: Container[int]
str_container: Container[str]
reveal_type(int_container.set_value(42)) # => Container[int]
reveal_type(str_container.set_value("hello")) # => Container[str]
def object_with_generic_type(
container: Container[T], value: T,
) -> Container[T]:
return container.set_value(value) # => Container[T]
しかし、ここではset_value
メソッドでself.value
の正確な型を指定しない。
ある型チェッカーではTypeVar(“Self”, bound=Container[T])
とclass-localな型変数を使ってSelf
とすると、正確な型T
が推論されるかもしれない。
ただし、class-localな型変数は標準の型システムの機能ではないので、self.value
にAny
を推論させることもできる。これは型チェッカーの実装に委ねられている。
ここで、Self[int]
のような型引数でSelf
を使うのは許されない。不必要にself
引数の型が曖昧になり、複雑になるからで
class Container(Generic[T]):
def foo(
self, other: Self[int], other2: Self,
) -> Self[str]: # Rejected
...
こんな時は、明示的に書くことを推奨される
class Container(Generic[T]):
def foo(
self: Container[T],
other: Container[int],
other2: Container[T]
) -> Container[str]: ...
Protocolでも使える
Protocolの中でも
from typing import Protocol, Self
class ShapeProtocol(Protocol):
scale: float
def set_scale(self, scale: float) -> Self:
self.scale = scale
return self
の用に使えて、これはtyping.TypeVar
を使って(ry
from typing import Protocol, Self
class ShapeProtocol(Protocol):
scale: float
def set_scale(self, scale: float) -> Self:
self.scale = scale
return self
ProtocolにバインドされたTypeVars
の動作の詳細については、PEP 544を参照。
特定のclassがprotocolと互換性があるかチェックする時は、もしprotocolがメソッドやAttributeの型付けをSelf
でしている場合、または対応するメソッドやAttributeの型付けがSelf
か具象型Foo
かそのサブクラスになっていれば、クラスFoo
はそのprotocolと互換性があることになる。
from typing import Protocol
class ShapeProtocol(Protocol):
def set_scale(self, scale: float) -> Self: ...
class ReturnSelf:
scale: float = 1.0
def set_scale(self, scale: float) -> Self:
self.scale = scale
return self
class ReturnConcreteShape:
scale: float = 1.0
def set_scale(self, scale: float) -> ReturnConcreteShape:
self.scale = scale
return self
class BadReturnType:
scale: float = 1.0
def set_scale(self, scale: float) -> int:
self.scale = scale
return 42
class ReturnDifferentClass:
scale: float = 1.0
def set_scale(self, scale: float) -> ReturnConcreteShape:
return ReturnConcreteShape(...)
def accepts_shape(shape: ShapeProtocol) -> None:
y = shape.set_scale(0.5)
reveal_type(y)
def main() -> None:
return_self_shape: ReturnSelf
return_concrete_shape: ReturnConcreteShape
bad_return_type: BadReturnType
return_different_class: ReturnDifferentClass
accepts_shape(return_self_shape) # OK
accepts_shape(return_concrete_shape) # OK
accepts_shape(bad_return_type) # だめ
# サブクラスを返さないのでこれはだめ
accepts_shape(return_different_class)
`Self`に型付けできる場所
Self
で型付けすることはclassのコンテキストだけで有効なので、常にカプセル化されたclassを参照する。
ネストされたclassを含むコンテキストでは、Self
は常に一番内側のclassを参照する。
class ReturnsSelf:
def foo(self) -> Self: ... # OK
@classmethod
def bar(cls) -> Self: # OK
return cls()
def __new__(cls, value: int) -> Self: ... # OK
def explicitly_use_self(self: Self) -> Self: ... # OK
# OK (他の型にネストもできる)
def returns_list(self) -> list[Self]: ...
# OK (同上)
@classmethod
def return_cls(cls) -> type[Self]:
return cls
class Child(ReturnsSelf):
# OK (Selfを使うメソッドを上書きできる)
def foo(self) -> Self: ...
class TakesSelf:
def foo(self, other: Self) -> bool: ... # OK
class Recursive:
# OK (SelfかNoneを返す@propertyとして扱われる)
next: Self | None
class CallableAttribute:
def foo(self) -> int: ...
# OK (@property returning the Callableを返す@propertyとして扱われる)
bar: Callable[[Self], int] = foo
class HasNestedFunction:
x: int = 42
def foo(self) -> None:
# OK (HasNestedFunctionに束縛される)
def nested(z: int, inner_self: Self) -> Self:
print(z)
print(inner_self.x)
return inner_self
nested(42, self) # OK
class Outer:
class Inner:
def foo(self) -> Self: ... # OK (Innerに束縛される)
ただし、次のようなものは型チェッカーによって怒られてしまう。
def foo(bar: Self) -> Self: ... # classの中でないのでダメ
bar: Self # ダメ (同上)
class Foo:
# Unknownとして扱われるのでダメ
def has_existing_self_annotation(self: T) -> Self: ...
class Foo:
def return_concrete_type(self) -> Self:
return Foo() # ダメ (下記参照)
class FooChild(Foo):
child_value: int = 42
def child_method(self) -> None:
# 実行時評価ではFooになってしまう
y = self.return_concrete_type()
y.child_value
# RuntimeError: Foo has no attribute child_value
class Bar(Generic[T]):
def bar(self) -> T: ...
class Baz(Bar[Self]): ... # 循環参照が起こるのでダメ
Self
を含む型エイリアスも許可されない。定義の外側でSelf
をサポートするのは、型チェッカーに特別な処理を必要とする可能性がある。
classの外側でSelf
を使うのは他の部分にも反することで、エイリアスの利便性を高めることは見合わない。
TupleSelf = Tuple[Self, Self] # だめ
class Alias:
def return_tuple(self) -> TupleSelf: # だめ
return (self, self)
同じ理由で、メタクラスで使うことも許されていない。
メタクラスでは、異なるメソッドのシグネチャで違う型を参照する必要がある。例えば__mul__
は戻り値の型Self
は包含クラスMyMetaclass
ではなくて実装のFoo
を参照する。一方で__new__
では戻り値の型Self
はメタクラスを参照するので、混乱の元になる。
class MyMetaclass(type):
def __new__(cls, *args: Any) -> Self: # MyMetaclassを返す
return super().__new__(cls, *args)
def __mul__(cls, count: int) -> list[Self]: # Fooを返す
return [cls()] * count
class Foo(metaclass=MyMetaclass): ...
実装上の振る舞い
Self
はsubscriptableではないので、typing.NoReturn
と同様の実装になる
@_SpecialForm
def Self(self, params):
"""Used to spell the type of "self" in classes.
Example::
from typing import Self
class ReturnsSelf:
def parse(self, data: bytes) -> Self:
...
return self
"""
raise TypeError(f"{self} is not subscriptable")
リファレンス実装 (訳文ママ)
- Mypyの実装例: https://github.com/Gobot1234/mypy/commit/ff779e8f30eed84b98c1d374c9d5666ac3b29b73
- Pyright: v1.1.184で対応
- CPythonの実装: https://github.com/python/typing/pull/933
資料 (訳文ママ)
2016年ごろからMypyでも同様の議論が始まっていた。
しかし、そこで最終的に取られたアプローチは「以前は...」の例で示したような冗長なtyping.TypeVar
を使うものだった。
これについて議論している他のissueには、Mypy#2354がある。
PradeepはPyCon Typing Summit 2021で具体的な提案をしました。(動画 資料)
James は typing-sig で独自にこの提案を持ち出しました。該当のメールスレッド
TypeScriptにはthis
型があり、RustにはSelf
型があります。
最後に、このPEPにフィードバックをくれた方たちに謝辞を述べます。
Jia Chen, Rebecca Chen, Sergei Lebedev, Kaylynn Morgan, Tuomas Suutari, Eric Traut, Alex Waygood, Shannon Zhu, and Никита Соболев
長すぎ!
Author And Source
この問題について(Python 3.11の型ヒントに導入される"PEP 673 – Self Type"を訳してみた), 我々は、より多くの情報をここで見つけました https://zenn.dev/peacock0803sz/articles/cf7ba5b3ef396b著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol