Self в аннотациях типов Python

Раздел: Основы 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, а не MyClass

Python 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)  # тип - ChildClass

Python 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 self

Python 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, pepperoni
Pizza 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 предпочтительнее.

Тип self в Python - comments

En
Python type self (python)