파이썬200번5 MIN
Self 타입: 메서드 체이닝과 자기 참조
Self 타입으로 메서드 체이닝과 팩토리 메서드를 서브클래스에서도 정확히 추론하게 만드는 법, TypeVar bound 방식과의 비교까지 완전 정복합니다.
이 글은 PALDYN Tech Blog의 글입니다.원문 보기
지난 글에서 런타임 타입 검사의 가능과 한계를 살펴봤다. 이번 글에서는 자기 자신을 반환하는 메서드의 타입을 우아하게 표현하는 Self 타입을 다룬다. 빌더 패턴이나 메서드 체이닝을 구현해 본 적이 있다면, "이 메서드의 반환 타입을 어떻게 적지?"라는 고민을 한 번쯤 해 봤을 것이다.
문제: 클래스 이름으로 적으면 깨진다
메서드가 self를 반환할 때, 단순히 클래스 이름을 반환 타입으로 적으면 서브클래스에서 문제가 생긴다.
class Query:
def filter(self, **kw) -> "Query": # 클래스 이름으로 명시
return self
class UserQuery(Query):
def only_active(self) -> "UserQuery":
return self
q = UserQuery().filter(active=True)
q.only_active() # 오류: filter()의 반환은 Query라 only_active가 없다
filter의 반환 타입을 Query로 적었기 때문에, UserQuery에서 호출해도 검사기는 결과를 Query로만 본다. 그래서 서브클래스 고유 메서드 only_active를 쓸 수 없다.
Self가 답이다
파이썬 3.11에 추가된 Self는 "이 메서드를 호출한 실제 클래스"를 가리킨다. 베이스 클래스에 한 번만 적어도, 서브클래스에서 호출하면 자동으로 그 서브클래스 타입으로 추론된다.
from typing import Self
class Query:
def __init__(self) -> None:
self._where: dict = {}
def filter(self, **kw) -> Self:
self._where.update(kw)
return self # Self 반환
class UserQuery(Query): ...
q = UserQuery().filter(age=30) # q: UserQuery 으로 추론
UserQuery().filter(...)의 결과가 UserQuery로 추론되므로, 이어서 서브클래스 메서드를 호출해도 검사기가 통과시킨다.
메서드 체이닝
Self가 가장 빛나는 곳은 메서드 체이닝이다. 각 메서드가 Self를 반환하면, 호출을 점으로 계속 이어 가도 타입이 정확히 유지된다.
from typing import Self
class Query:
def filter(self, **kw) -> Self: ...
def order_by(self, field: str) -> Self: ...
def limit(self, n: int) -> Self: ...
result = (
Query()
.filter(active=True)
.order_by("name")
.limit(10)
)
체인의 모든 단계에서 타입이 Query(혹은 서브클래스)로 유지되므로, 자동완성과 검사가 끝까지 작동한다.
팩토리 메서드와 classmethod
Self는 인스턴스를 만들어 반환하는 classmethod에서도 유용하다. 대안 생성자를 만들 때 서브클래스 타입을 정확히 반환한다.
from typing import Self
class Model:
@classmethod
def from_dict(cls, data: dict) -> Self:
obj = cls()
obj.__dict__.update(data)
return obj
class User(Model): ...
u = User.from_dict({"name": "Sam"}) # u: User 으로 추론
from_dict를 Model에 한 번만 정의해도, User.from_dict(...)는 User를 반환하는 것으로 추론된다.
TypeVar bound 방식과 비교
Self가 등장하기 전에는 TypeVar의 bound로 같은 효과를 냈다. 비교해 두면 옛 코드를 읽을 때 도움이 된다.
from typing import TypeVar
T = TypeVar("T", bound="Query")
class Query:
def filter(self: T, **kw) -> T: # 옛 방식
return self
self: T 형태로 메서드의 self에 타입 변수를 바인딩하는 방식인데, 장황하고 직관적이지 않다. 3.11 이상이라면 Self가 훨씬 짧고 읽기 쉬우므로 새 코드에서는 Self를 쓰는 것이 정석이다.
기본 어노테이션에서 제네릭, Union, Literal·Final, TypedDict, Protocol, ParamSpec, 검사 도구, 런타임 검증, 그리고 Self까지 — 파이썬의 점진적 타이핑은 코드를 실행하지 않고도 수많은 버그를 미리 잡아 준다. 작은 함수 하나부터 힌트를 달아 보면, 그 안정감이 곧 습관이 될 것이다.
지난 글: 런타임 타입 검사: runtime_checkable과 그 한계
읽어주셔서 감사합니다. 😊

