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을 Self[int]와 같은 타입 인자와 함께 사용하는 것을 거부한다는 점에 유의하십시오. 이는 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을 사용한다면, 해당 메서드와 속성 어노테이션이 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
명시적인 것이 암묵적인 것보다 낫기 때문에 이 제안을 거부합니다. 또한 위 접근 방식은 분석할 메서드 본문이 없는 타입 스텁에서는 작동하지 않습니다.
참조 구현
Mypy: Mypy의 개념 증명 구현입니다.
Pyright: v1.1.184
Self의 런타임 구현: PR입니다.
자료
Python에서 Self 타입에 대한 유사한 논의는 2016년경 Mypy에서 시작되었습니다: Mypy issue #1212 - SelfType 또는 “self의 타입”을 표기하는 또 다른 방법입니다. 그러나 최종적으로 그곳에서 채택된 접근 방식은 “이전” 예제에 표시된 제한된 TypeVar 접근 방식이었습니다. 이를 논의하는 다른 이슈로는 Mypy issue #2354 - 제네릭 클래스의 Self 타입이 있습니다.
- Pradeep은 PyCon Typing Summit 2021에서 구체적인 제안을 했습니다.
- recorded talk, slides입니다.
James는 typing-sig에서 독립적으로 이 제안을 제기했습니다: Typing-sig thread.
다른 언어에도 바깥 클래스의 타입을 표현하는 유사한 방법이 있습니다.
- TypeScript에는
this타입이 있습니다(TypeScript docs) - Rust에는
Self타입이 있습니다(Rust docs)
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.