Self в аннотациях типов Python
Основной подход: использование Self из модуля typing
Самый эффективный и современный способ аннотировать возвращаемый тип методов, которые должны возвращать экземпляр текущего класса (включая подклассы) - это тип Self, доступный начиная с Python 3.11. Он автоматически учитывает наследование, возвращая точный тип вызывающего класса.
from typing import Self
class MyClass:
def set_value(self, value: int) -> Self:
self.value = value
return self
class ChildClass(MyClass):
pass
obj = ChildClass().set_value(10)
# Тип obj будет ChildClass, а не MyClassPython type self (тип self в python)
Ключевое преимущество: при наследовании сигнатура метода в дочернем классе не переопределяется, но статический анализ (mypy, Pyright) правильно определяет тип возвращаемого значения как дочерний класс.
Как аннотировать возвращаемый тип метода, чтобы подклассы не теряли свою типизацию?
До появления Self использовался приём с TypeVar, ограниченным текущим классом. Этот способ менее удобен, но работает во всех версиях Python.
from typing import TypeVar
T = TypeVar('T', bound='MyClass')
class MyClass:
def set_value(self: T, value: int) -> T:
self.value = value
return self
class ChildClass(MyClass):
pass
obj = ChildClass().set_value(42) # тип - ChildClassPython typing function (аннотация типа функции в python)
Здесь параметр self аннотируется как T, а возвращаемый тип - тот же T. Это вынуждает писать self: T явно, что необычно, но решает задачу.
Возможные проблемы:
- TypeVar требует указания bound, иначе self может быть любого типа.
- При множественном наследовании или generic-классах конструкция усложняется.
- Инструменты статического анализа могут выдавать предупреждения, если self не аннотирован строго.
Как обойтись без импорта дополнительных типов, используя только имя класса?
Можно аннотировать возвращаемый тип строкой с именем класса (forward reference). Это интуитивно понятно, но не работает при наследовании - метод в дочернем классе всё равно вернёт тип родителя.
class MyClass:
def copy(self) -> 'MyClass':
return MyClass()
class ChildClass(MyClass):
pass
obj = ChildClass().copy() # тип obj - MyClass, хотя хотелось бы ChildClass
Python 3.12 type (новые возможности типов в python 3.12)
Этот способ подходит, если метод всегда создаёт или возвращает именно экземпляр того класса, в котором определён (т.е. нет наследования или метод не возвращает self). В противном случае типизация будет неверна.
Типичная ошибка:
Путаница с self как параметром: в аннотации метода self не нужно указывать тип явно, если не используется TypeVar. Попытка написать def method(self: 'MyClass') -> 'MyClass' избыточна и не даёт преимуществ.
Как обеспечить совместимость со старыми версиями Python (до 3.11)?
Библиотека typing_extensions предоставляет тип Self для Python 3.8+. Установите её через pip и импортируйте оттуда. Функциональность полностью совпадает со встроенной.
# pip install typing_extensions
from typing_extensions import Self
class MyClass:
def reset(self) -> Self:
self.value = 0
return selfPython typing type checking (проверка типов с помощью typing в python)
Это наиболее удобный путь для проектов, которые не могут перейти на Python 3.11.
Некоторые старые анализаторы (например, старые версии mypy) могут не поддерживать Self даже из typing_extensions. В таком случае используйте TypeVar.
Как аннотировать методы, возвращающие экземпляр другого класса (фабричные методы)?
Здесь Self не подходит, так как возвращается не текущий класс. Используйте явное указание возвращаемого класса или TypeVar для общего случая. Например:
class Database:
def connect(self) -> Connection:
return Connection()Если фабричный метод полиморфный, можно параметризовать возвращаемый тип с помощью TypeVar.
Расширенные примеры использования Self и альтернатив
Реализация цепочечного вызова методов (builder pattern)
from typing import Self
class PizzaBuilder:
def __init__(self):
self.toppings = []
def add_cheese(self) -> Self:
self.toppings.append('cheese')
return self
def add_pepperoni(self) -> Self:
self.toppings.append('pepperoni')
return self
def build(self) -> str:
return 'Pizza with ' + ', '.join(self.toppings)
# Использование
pizza = PizzaBuilder().add_cheese().add_pepperoni().build()
print(pizza) # Pizza with cheese, pepperoniPizza with cheese, pepperoni
Без Self при наследовании цепочка ломалась бы. Дочерний класс может расширить функциональность, сохранив тип.
Абстрактный базовый класс с возвратом self
from abc import ABC, abstractmethod
from typing import Self
class Shape(ABC):
@abstractmethod
def move(self, dx: float, dy: float) -> Self:
...
class Circle(Shape):
def move(self, dx: float, dy: float) -> Self:
self.x += dx
self.y += dy
return self
circle = Circle()
moved = circle.move(10, 20) # тип moved - CircleАннотация Self в абстрактном методе гарантирует, что конкретная реализация вернёт свой тип.
Generic-класс с Self
from typing import Generic, TypeVar, Self
T = TypeVar('T')
class Container(Generic[T]):
def __init__(self, value: T):
self.value = value
def update(self, new_value: T) -> Self:
self.value = new_value
return self
class IntContainer(Container[int]):
def double(self) -> Self:
self.value *= 2
return self
c = IntContainer(5).update(10).double() # c -> IntContainer[int]Self корректно сохраняет информацию о generic-параметре, в данном случае int.
Обходное решение для Python 3.8-3.10 с использованием __class__
from typing import TypeVar
T = TypeVar('T')
class Base:
def create(self: T) -> T:
# Неявно возвращаем self, но тип выводится из аннотации
return self
class Derived(Base):
pass
d = Derived().create() # mypy выведет DerivedДругой приём - использовать __class__ для возврата экземпляра:
class Base:
def create(self) -> 'Base':
return object.__new__(self.__class__)
# Но такая аннотация не точна для подклассовSelf или TypeVar предпочтительнее.