PEP 673 – Self 타입
- Author:
- Pradeep Kumar Srinivasan <gohanpra at gmail.com>, James Hilton-Balfe <gobot1234yt at gmail.com>
- Sponsor:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- Discussions-To:
- Typing-SIG list
- Status:
- Final
- Type:
- Standards Track
- Topic:
- Typing
- Created:
- 10-Nov-2021
- Python-Version:
- 3.11
- Post-History:
- 17-Nov-2021
- Resolution:
- Python-Dev thread
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 자기 클래스의 인스턴스를 반환하는 메서드에 어노테이션을 다는 간단하고 직관적인 방법을 소개합니다. 이 방법은 PEP 484에 명시된 TypeVar 기반 접근 방식과 똑같이 동작하지만, 더 간결하고 이해하기 쉽습니다.
동기
흔한 사용 사례는 같은 클래스의 인스턴스를 반환하는 메서드를 작성하는 것이며, 보통 self를 반환하는 방식으로 작성합니다.
class Shape:
def set_scale(self, scale: float):
self.scale = scale
return self
Shape().set_scale(0.5) # => should be Shape
반환 타입을 나타내는 한 가지 방법은 반환 타입을 현재 클래스, 예컨대 Shape로 지정하는 것입니다. 이 메서드를 사용하면 타입 검사기는 예상대로 Shape 타입을 추론합니다.
class Shape:
def set_scale(self, scale: float) -> Shape:
self.scale = scale
return self
Shape().set_scale(0.5) # => Shape
그러나 Shape의 서브클래스에서 set_scale을 호출하면, 타입 검사기는 여전히 반환 타입을 Shape로 추론합니다. 이는 아래와 같은 상황에서 문제가 됩니다. 베이스 클래스에 없는 속성이나 메서드를 사용하려 하므로 타입 검사기가 오류를 반환하기 때문입니다.
class Circle(Shape):
def set_radius(self, r: float) -> Circle:
self.radius = r
return self
Circle().set_scale(0.5) # *Shape*, not Circle
Circle().set_scale(0.5).set_radius(2.7)
# => Error: Shape has no attribute set_radius
이러한 경우에 현재 쓰이는 우회 방법은 베이스 클래스를 바운드로 하는 TypeVar를 정의하고, 이를 self 매개변수와 반환 타입의 어노테이션으로 사용하는 것입니다:
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
안타깝게도 이 방법은 장황하고 직관적이지 않습니다. self에는 보통 명시적으로 어노테이션을 달지 않기 때문에 위의 해결책이 곧바로 떠오르지 않으며, 떠오르더라도 TypeVar(bound="Shape")의 바운드나 self에 대한 어노테이션 중 하나를 빠뜨려 실수하기가 매우 쉽습니다.
이러한 어려움 때문에 사용자는 종종 포기하고 Any 같은 대체 타입을 사용하거나 타입 어노테이션을 아예 생략하는데, 두 방법 모두 코드의 안전성을 떨어뜨립니다.
저희는 위의 의도를 더 직관적이고 간결하게 표현하는 방법을 제안합니다. 감싸고 있는 클래스에 바운드된 타입 변수를 나타내는 특수 형식 Self를 도입합니다. 위와 같은 상황에서 사용자는 반환 타입에 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
반환 타입에 Self로 어노테이션을 달면, 베이스 클래스를 명시적 바운드로 하는 TypeVar를 더 이상 선언할 필요가 없습니다. 반환 타입 Self는 함수가 self를 반환한다는 사실을 그대로 반영하며 이해하기도 더 쉽습니다.
위의 예제에서와 같이, 타입 검사기는 예상대로 Circle().set_scale(0.5)의 타입을 Circle로 올바르게 추론합니다.
사용 통계
저희가 인기 있는 오픈 소스 프로젝트들을 분석한 결과, 위와 같은 패턴은 dict나 Callable 같은 인기 있는 타입에 비해 약 40% 정도의 빈도로 사용되었습니다. 예를 들어 typeshed 한 곳에서만 이러한 “Self” 타입이 523번 사용되었으며, 이는 dict의 1286번, Callable의 1314번 사용과 비교되는 수치입니다(2021년 10월 기준). 이는 Self 타입이 꽤 자주 사용될 것이며 사용자가 위의 더 간단한 접근 방식으로부터 큰 이점을 얻을 것임을 시사합니다.
명세
메서드 시그니처에서의 사용
메서드 시그니처에 사용된 Self는 해당 클래스에 바운드된 TypeVar인 것처럼 취급됩니다.
from typing import Self
class Shape:
def set_scale(self, scale: float) -> Self:
self.scale = scale
return self
이는 다음과 동등하게 취급됩니다:
from typing import TypeVar
SelfShape = TypeVar("SelfShape", bound="Shape")
class Shape:
def set_scale(self: SelfShape, scale: float) -> SelfShape:
self.scale = scale
return self
서브클래스에서도 똑같이 동작합니다:
class Circle(Shape):
def set_radius(self, radius: float) -> Self:
self.radius = radius
return self
이는 다음과 동등하게 취급됩니다:
SelfCircle = TypeVar("SelfCircle", bound="Circle")
class Circle(Shape):
def set_radius(self: SelfCircle, radius: float) -> SelfCircle:
self.radius = radius
return self
한 가지 구현 전략은 전처리 단계에서 전자를 후자로 단순히 디슈가링하는 것입니다. 메서드가 시그니처에서 Self를 사용하면, 메서드 내부에서 self의 타입은 Self가 됩니다. 그 밖의 경우에는 self의 타입이 그 메서드를 둘러싼 클래스로 유지됩니다.
클래스 메서드 시그니처에서의 사용
Self 타입 어노테이션은 자신이 작동하는 클래스의 인스턴스를 반환하는 클래스 메서드에도 유용합니다. 예를 들어, 다음 코드 조각의 from_config는 주어진 config로부터 Shape 객체를 생성합니다.
class Shape:
def __init__(self, scale: float) -> None: ...
@classmethod
def from_config(cls, config: dict[str, float]) -> Shape:
return cls(config["scale"])
그러나 이는 Circle.from_config(...)가 실제로는 Circle 타입의 값을 반환해야 하는데도 Shape 타입의 값을 반환한다고 추론된다는 뜻입니다:
class Circle(Shape):
def circumference(self) -> float: ...
shape = Shape.from_config({"scale": 7.0})
# => Shape
circle = Circle.from_config({"scale": 7.0})
# => *Shape*, not Circle
circle.circumference()
# Error: `Shape` has no attribute `circumference`
현재 이에 대한 우회 방법은 직관적이지 않고 오류가 발생하기 쉽습니다:
Self = TypeVar("Self", bound="Shape")
class Shape:
@classmethod
def from_config(
cls: type[Self], config: dict[str, float]
) -> Self:
return cls(config["scale"])
저희는 Self를 직접 사용할 것을 제안합니다:
from typing import Self
class Shape:
@classmethod
def from_config(cls, config: dict[str, float]) -> Self:
return cls(config["scale"])
이렇게 하면 복잡한 cls: type[Self] 어노테이션과 bound를 지정한 TypeVar 선언을 피할 수 있습니다. 이번에도 후자의 코드는 전자의 코드와 동일하게 동작합니다.
매개변수 타입에서의 사용
Self의 또 다른 용도는 현재 클래스의 인스턴스를 기대하는 매개변수에 어노테이션을 다는 것입니다:
Self = TypeVar("Self", bound="Shape")
class Shape:
def difference(self: Self, other: Self) -> float: ...
def apply(self: Self, f: Callable[[Self], None]) -> None: ...
저희는 같은 동작을 얻기 위해 Self를 직접 사용할 것을 제안합니다:
from typing import Self
class Shape:
def difference(self, other: Self) -> float: ...
def apply(self, f: Callable[[Self], None]) -> None: ...
self: Self를 지정해도 해가 없으므로, 일부 사용자는 위 코드를 다음과 같이 작성하는 편이 더 읽기 쉽다고 느낄 수 있습니다:
class Shape:
def difference(self: Self, other: Self) -> float: ...
속성 어노테이션에서의 사용
Self의 또 다른 용도는 속성에 어노테이션을 다는 것입니다. 한 가지 예로, 요소가 현재 클래스의 서브클래스여야 하는 LinkedList가 있는 경우를 들 수 있습니다.
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))
# Not OK
LinkedList[int](value=1, next=LinkedList[str](value="hello"))
그러나 next 속성을 LinkedList[T]로 어노테이션하면 서브클래스를 사용한 잘못된 구성이 허용됩니다:
@dataclass
class OrdinalLinkedList(LinkedList[int]):
def ordinal_value(self) -> str:
return as_ordinal(self.value)
# Should not be OK because LinkedList[int] is not a subclass of
# OrdinalLinkedList, # but the type checker allows it.
xs = OrdinalLinkedList(value=1, next=LinkedList[int](value=2))
if xs.next:
print(xs.next.ordinal_value()) # Runtime Error.
저희는 이 제약을 next: Self | None을 사용하여 표현할 것을 제안합니다:
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))
# Type error: 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) # Not OK
위 코드는 Self 타입을 포함하는 각 속성을 해당 타입을 반환하는 property로 취급하는 것과 의미상 동일합니다:
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)
제네릭 클래스에서의 사용
Self는 제네릭 클래스 메서드에서도 사용할 수 있습니다:
class Container(Generic[T]):
value: T
def set_value(self, value: T) -> Self: ...
이는 다음과 같이 작성하는 것과 동일합니다:
Self = TypeVar("Self", bound="Container[Any]")
class Container(Generic[T]):
value: T
def set_value(self: Self, value: T) -> Self: ...
이 동작은 메서드가 호출된 객체의 타입 인자를 보존합니다. 구체적인 타입이 Container[int]인 객체에서 호출되면 Self는 Container[int]에 바인딩됩니다. 제네릭 타입이 Container[T]인 객체로 호출되면 Self는 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]
이 PEP는 set_value 메서드 내에서 self.value의 정확한 타입을 명시하지 않습니다. 일부 타입 검사기는 Self = TypeVar(“Self”, bound=Container[T])와 같은 클래스 지역 타입 변수를 사용하여 Self 타입을 구현할 수 있으며, 이 경우 정확한 타입 T가 추론됩니다. 그러나 클래스 지역 타입 변수는 표준화된 타입 시스템 기능이 아니므로, self.value의 타입을 Any로 추론하는 것도 허용됩니다. 이는 타입 검사기의 재량에 맡깁니다.
Self[int]와 같이 Self를 타입 인자와 함께 사용하는 것은 거부한다는 점에 유의하십시오. 이는 self 매개변수의 타입에 모호성을 만들고 불필요한 복잡성을 도입하기 때문입니다:
class Container(Generic[T]):
def foo(
self, other: Self[int], other2: Self,
) -> Self[str]: # Rejected
...
이러한 경우에는 self에 명시적인 타입을 사용할 것을 권장합니다:
class Container(Generic[T]):
def foo(
self: Container[T],
other: Container[int],
other2: Container[T]
) -> Container[str]: ...
프로토콜에서의 사용
Self는 클래스에서 사용하는 것과 마찬가지로 프로토콜 내에서도 유효합니다:
from typing import Protocol, Self
class ShapeProtocol(Protocol):
scale: float
def set_scale(self, scale: float) -> Self:
self.scale = scale
return self
위 코드는 다음과 동등하게 취급됩니다:
from typing import TypeVar
SelfShape = TypeVar("SelfShape", bound="ShapeProtocol")
class ShapeProtocol(Protocol):
scale: float
def set_scale(self: SelfShape, scale: float) -> SelfShape:
self.scale = scale
return self
프로토콜에 바운드된 TypeVar의 동작에 대한 자세한 내용은 PEP 544를 참조하십시오.
클래스가 프로토콜과 호환되는지 검사하기: 프로토콜이 메서드나 속성 어노테이션에서 Self를 사용하는 경우, 클래스 Foo의 해당 메서드와 속성 어노테이션이 Self나 Foo, 또는 Foo의 서브클래스 중 하나를 사용하면 클래스 Foo는 프로토콜과 호환되는 것으로 간주됩니다. 아래 예제를 참조하십시오:
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) # Not OK
# Not OK because it returns a non-subclass.
accepts_shape(return_different_class)
Self의 유효한 위치
Self 어노테이션은 클래스 문맥에서만 유효하며, 항상 자신을 감싸는 클래스를 가리킵니다. 중첩 클래스가 관련된 문맥에서 Self는 항상 가장 안쪽의 클래스를 가리킵니다.
다음과 같은 Self 사용은 허용됩니다:
class ReturnsSelf:
def foo(self) -> Self: ... # Accepted
@classmethod
def bar(cls) -> Self: # Accepted
return cls()
def __new__(cls, value: int) -> Self: ... # Accepted
def explicitly_use_self(self: Self) -> Self: ... # Accepted
# Accepted (Self can be nested within other types)
def returns_list(self) -> list[Self]: ...
# Accepted (Self can be nested within other types)
@classmethod
def return_cls(cls) -> type[Self]:
return cls
class Child(ReturnsSelf):
# Accepted (we can override a method that uses Self annotations)
def foo(self) -> Self: ...
class TakesSelf:
def foo(self, other: Self) -> bool: ... # Accepted
class Recursive:
# Accepted (treated as an @property returning ``Self | None``)
next: Self | None
class CallableAttribute:
def foo(self) -> int: ...
# Accepted (treated as an @property returning the Callable type)
bar: Callable[[Self], int] = foo
class HasNestedFunction:
x: int = 42
def foo(self) -> None:
# Accepted (Self is bound to 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: ... # Accepted (Self is bound to Inner)
다음과 같은 Self 사용은 거부됩니다.
def foo(bar: Self) -> Self: ... # Rejected (not within a class)
bar: Self # Rejected (not within a class)
class Foo:
# Rejected (Self is treated as unknown).
def has_existing_self_annotation(self: T) -> Self: ...
class Foo:
def return_concrete_type(self) -> Self:
return Foo() # Rejected (see FooChild below for rationale)
class FooChild(Foo):
child_value: int = 42
def child_method(self) -> None:
# At runtime, this would be Foo, not FooChild.
y = self.return_concrete_type()
y.child_value
# Runtime error: Foo has no attribute child_value
class Bar(Generic[T]):
def bar(self) -> T: ...
class Baz(Bar[Self]): ... # Rejected
Self를 포함하는 타입 별칭은 거부합니다. 클래스 정의 바깥에서 Self를 지원하려면 타입 검사기에서 많은 특수 처리가 필요할 수 있습니다. 클래스 정의 바깥에서 Self를 사용하는 것은 이 PEP의 나머지 부분과도 어긋난다는 점을 고려할 때, 별칭이 주는 추가적인 편리함은 그만한 가치가 없다고 판단합니다:
TupleSelf = Tuple[Self, Self] # Rejected
class Alias:
def return_tuple(self) -> TupleSelf: # Rejected
return (self, self)
정적 메서드에서의 Self도 거부한다는 점에 유의하십시오. 반환할 self나 cls가 없으므로 Self가 더하는 가치는 크지 않습니다. 가능한 사용 사례는 매개변수 자체를 반환하거나, 매개변수로 전달된 컨테이너의 어떤 요소를 반환하는 것뿐입니다. 이러한 경우는 추가되는 복잡성을 감수할 만한 가치가 없어 보입니다.
class Base:
@staticmethod
def make() -> Self: # Rejected
...
@staticmethod
def return_parameter(foo: Self) -> Self: # Rejected
...
마찬가지로 메타클래스에서의 Self도 거부합니다. 이 PEP에서 Self는 일관되게 같은 타입(self의 타입)을 가리킵니다. 그러나 메타클래스에서는 메서드 시그니처마다 서로 다른 타입을 가리켜야 합니다. 예를 들어 __mul__에서 반환 타입의 Self는 감싸는 클래스인 MyMetaclass가 아니라 구현하는 클래스인 Foo를 가리키게 됩니다. 하지만 __new__에서 반환 타입의 Self는 감싸는 클래스인 MyMetaclass를 가리키게 됩니다. 혼란을 피하기 위해 이러한 예외적인 경우는 거부합니다.
class MyMetaclass(type):
def __new__(cls, *args: Any) -> Self: # Rejected
return super().__new__(cls, *args)
def __mul__(cls, count: int) -> list[Self]: # Rejected
return [cls()] * count
class Foo(metaclass=MyMetaclass): ...
런타임 동작
Self는 서브스크립트할 수 없으므로 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")
기각된 대안
타입 검사기가 반환 타입을 추론하도록 허용하기
한 가지 제안은 Self 타입을 암시적으로 남겨 두고, 반환 타입이 self 매개변수의 타입과 같아야 한다는 것을 타입 검사기가 메서드 본문으로부터 추론하도록 하는 것입니다:
class Shape:
def set_scale(self, scale: float):
self.scale = scale
return self # Type checker infers that we are returning self
명시적인 것이 암시적인 것보다 낫기(Explicit Is Better Than Implicit) 때문에 이 제안은 거부합니다. 게다가 위의 접근 방식은 분석할 메서드 본문이 없는 타입 스텁에서는 실패합니다.
참조 구현
Mypy: 개념 증명 구현은 Mypy에 있습니다.
Pyright: v1.1.184
Self의 런타임 구현: PR.
참고 자료
Python의 Self 타입에 관한 비슷한 논의는 2016년 무렵 Mypy에서 시작되었습니다: Mypy issue #1212 - SelfType or another way to spell “type of self”. 그러나 그곳에서 최종적으로 채택된 방식은 앞의 “변경 전” 예제에서 보인, 상한이 지정된 TypeVar 방식이었습니다. 이를 다루는 다른 이슈로는 Mypy issue #2354 - Self types in generic classes가 있습니다.
- Pradeep이 PyCon Typing Summit 2021에서 구체적인 제안을 발표했습니다:
- 발표 녹화, 슬라이드.
James가 typing-sig에서 독립적으로 이 제안을 제기했습니다: Typing-sig 스레드.
다른 언어에도 자신을 감싸는 클래스의 타입을 표현하는 비슷한 방법이 있습니다:
- TypeScript에는
this타입이 있습니다(TypeScript 문서). - Rust에는
Self타입이 있습니다(Rust 문서).
이 PEP에 의견을 보내 주신 다음 분들께 감사드립니다:
Jia Chen, Rebecca Chen, Sergei Lebedev, Kaylynn Morgan, Tuomas Suutari, Eric Traut, Alex Waygood, Shannon Zhu, Никита Соболев
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.