Типизация объектов: от класса до дженериков
Типизация классов в Python
Основной эффективный способ
Для типизации классов в Python применяется комбинация аннотаций атрибутов и методов с использованием модуля typing, в особенности классов Generic и TypeVar. Это позволяет создавать обобщенные классы, сохраняющие информацию о типе для статического анализа (mypy, Pyright).
from typing import TypeVar, Generic, List
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: List[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()Typing object python (typing.object в python)
Здесь Stack объявлен обобщённым с параметром T. Атрибут _items типизирован как List[T], методы явно указывают типы параметров и возвращаемого значения. При использовании Stack[int]() статический анализатор отслеживает согласованность.
Проблемы и ошибки
Аннотации не проверяются во время выполнения. Ошибка типа может проявиться только при запуске. Решение: применять библиотеки вроде pydantic или запускать mypy в CI/CD. Частая ошибка - использование TypeVar без указания bound, что может привести к неожиданным типам. Ограничивайте параметры через bound=SomeBase.
Как типизировать атрибуты класса и экземпляра?
Для атрибутов класса используется аннотация ClassVar. Атрибуты экземпляра аннотируются в __init__.
from typing import ClassVar
class Car:
wheels: ClassVar[int] = 4
def __init__(self, model: str, year: int) -> None:
self.model: str = model
self.year: int = yearPython typing string (typing.string в python)
Если попытаться присвоить ClassVar атрибуту экземпляра, mypy выдаст предупреждение. Для изменяемых атрибутов класса (список) изменения через экземпляр не отслеживаются статически.
Как описать интерфейс класса без наследования?
Используется Protocol из typing. Класс, имеющий нужные методы, автоматически удовлетворяет протоколу (утиная типизация с проверкой типов).
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Рисуем круг")
def render(obj: Drawable) -> None:
obj.draw()
render(Circle()) # OK
Python 3 typing (модуль typing в python 3)
Проблема: протоколы не поддерживают проверку атрибутов, только методы. Для атрибутов используйте @property в протоколе. Также протоколы с несколькими методами могут требовать явного декоратора @runtime_checkable для проверки isinstance.
Как создать класс со строгой структурой, как словарь?
Применяется TypedDict. Позволяет задать имена и типы ключей для классов, используемых как словари.
from typing import TypedDict
class Person(TypedDict):
name: str
age: int
p: Person = {'name': 'Alice', 'age': 30} # OK
# p['name'] = 123 # mypy ошибкаPython typing class (типизация классов в python)
TypedDict работает только со словарями, не с экземплярами. Для доступа к атрибутам через точку используйте NamedTuple или dataclass. При наследовании TypedDict нужно указывать total=False для необязательных ключей.
Как типизировать класс с неизменяемыми атрибутами?
Используйте NamedTuple из модуля typing. Он создаёт неизменяемый класс с аннотациями.
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
p = Point(1.0, 2.0)
print(p.x, p.y) # 1.0 2.0
# p.x = 3.0 # TypeErrorNamedTuple нельзя наследовать от других классов. Для наследования лучше использовать dataclass с frozen=True.
Как автоматически генерировать методы с типизацией?
@dataclass из модуля dataclasses создаёт __init__, __repr__, __eq__ и др. с учётом аннотаций.
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
pages: int = 0
b = Book("Война и мир", "Толстой", 1225)Поля с изменяемыми типами (list, dict) нужно инициализировать через field(default_factory=list), иначе все экземпляры будут разделять один объект. Аннотации не проверяются в рантайме (используйте pydantic для валидации).
Как типизировать класс с ограниченным набором значений?
Применяется Literal из typing. Позволяет указать точные допустимые значения.
from typing import Literal
class Config:
mode: Literal['dev', 'prod']
def __init__(self, mode: str) -> None:
self.mode = mode # нужна проверка в рантайме
c = Config('dev') # OK
# c = Config('test') # mypy ошибкаМодуль typing не проверяет значения во время выполнения. Необходимо добавить валидацию вручную. Literal поддерживает только строки, числа, булевы значения и None.
Как избежать циклических ссылок в аннотациях?
Помогает from __future__ import annotations. Аннотации становятся строками, отложенными до вызова get_type_hints().
from __future__ import annotations
from typing import Optional
class Node:
def __init__(self, value: int, next: Optional[Node] = None):
self.value = value
self.next = nextБез этого импорта в Python 3.9 и ниже возникает NameError, так как Node ещё не определён. Импорт откладывает вычисление типов, но может вызвать проблемы с библиотеками, анализирующими типы в runtime (pydantic).
Расширенные примеры типизации классов
Пример 1. Рекурсивный тип: бинарное дерево
from typing import Optional, Generic, TypeVar
T = TypeVar('T')
class TreeNode(Generic[T]):
def __init__(self, value: T,
left: Optional['TreeNode[T]'] = None,
right: Optional['TreeNode[T]'] = None):
self.value = value
self.left = left
self.right = right
# Использование
tree = TreeNode[int](1)
tree.left = TreeNode(2)
tree.right = TreeNode(3)
# дерево: TreeNode[int] с value=1, left=..., right=...
Аннотация Optional['TreeNode[T]'] с кавычками нужна для циклической ссылки (если from __future__ import annotations не используется).
Пример 2. Протокол с несколькими методами
from typing import Protocol, runtime_checkable
@runtime_checkable
class IterableWithLength(Protocol):
def __len__(self) -> int: ...
def __iter__(self): ...
# Проверка isinstance
def process(obj: IterableWithLength) -> None:
if isinstance(obj, IterableWithLength):
print(f"Длина: {len(obj)}")
process([1,2,3]) # OK, выведет "Длина: 3"
process(123) # mypy ошибка, и isinstance вернёт False
# Для списка: Длина: 3
Декоратор @runtime_checkable позволяет использовать isinstance, но работает только для протоколов с методами, не содержащими сигнатур с дженериками.
Пример 3. Использование Self для возврата текущего класса
from typing import Self
class Base:
@classmethod
def create(cls) -> Self:
return cls()
class Derived(Base):
pass
derived = Derived.create() # self имеет тип Derived, а не Base
# derived: Derived
Self (Python 3.11+) возвращает точный тип вызывающего класса. В более старых версиях используется TypeVar с bound.
Пример 4. Unpack и **kwargs с типизацией
from typing import TypedDict, Unpack
class PersonDict(TypedDict):
name: str
age: int
def create_person(**kwargs: Unpack[PersonDict]) -> None:
print(kwargs)
create_person(name='Alice', age=30) # OK
# create_person(name='Bob', age='30') # mypy ошибка
# {'name': 'Alice', 'age': 30}Это позволяет строго типизировать именованные аргументы, соответствующие TypedDict.
Пример 5. Ограниченный Generic с bound
from typing import TypeVar, Generic
class Shape:
def area(self) -> float:
return 0.0
T = TypeVar('T', bound=Shape)
class Container(Generic[T]):
def __init__(self, item: T) -> None:
self.item = item
def get_area(self) -> float:
return self.item.area()
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14 * self.radius ** 2
c = Container(Circle(10))
print(c.get_area()) # 314.0
# 314.0
Параметр T ограничен типом Shape. Любой класс, наследующий Shape, может быть использован, но не, например, int.
Пример 6. Использование TypeAlias для сложных типов
from typing import TypeAlias, Dict, List
JSON: TypeAlias = Dict[str, 'JSON | List[JSON] | str | int | float | bool | None']
class Config:
data: JSON
def __init__(self, data: JSON) -> None:
self.data = data
conf = Config({'key': 'value', 'nested': [1, 2]})
# OK, mypy принимает вложенную структуру
TypeAlias помогает избежать повторения сложных аннотаций. Кавычки нужны для рекурсивного определения (Python 3.11+ поддерживает рекурсивные TypeAlias без кавычек при from __future__ import annotations).