Generics (обобщенные типы) позволяют писать код, параметризованный типами. Вместо жесткого указания list[int] или list[str] вы описываете функцию, работающую с list[T] для любого T. Python 3.12 радикально упростил синтаксис generics.
Зачем нужны generics
Без generics невозможно точно типизировать универсальные функции:
# Without generics — type checker can't track the type through
def first(items: list) -> object:
return items[0]
result = first([1, 2, 3])
# result is 'object' — type checker lost the 'int' information
# result.bit_length() # Error: object has no attribute 'bit_length'
TypeVar (до Python 3.12)
Традиционный способ создания обобщенных типов:
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
"""Return the first element, preserving its type."""
return items[0]
# Type checker knows the exact return type
num: int = first([1, 2, 3]) # T = int
name: str = first(["a", "b"]) # T = str
# Bounded TypeVar — restrict to certain types
Number = TypeVar("Number", int, float)
def add(a: Number, b: Number) -> Number:
return a + b
add(1, 2) # OK: int
add(1.5, 2.5) # OK: float
# add("a", "b") # Error: str is not int or float
# Upper bound — T must be a subclass
from typing import TypeVar
Comparable = TypeVar("Comparable", bound="SupportsLessThan")
def min_value(a: Comparable, b: Comparable) -> Comparable:
return a if a < b else b
Новый синтаксис Python 3.12+
Python 3.12 ввел встроенный синтаксис для generics -- больше не нужен TypeVar:
# Python 3.12+ — type parameter syntax
def first[T](items: list[T]) -> T:
"""Return the first element."""
return items[0]
# Multiple type parameters
def zip_strict[T, U](a: list[T], b: list[U]) -> list[tuple[T, U]]:
"""Zip two lists with strict length check."""
if len(a) != len(b):
raise ValueError("Lists must have equal length")
return list(zip(a, b))
result = zip_strict([1, 2], ["a", "b"])
# result: list[tuple[int, str]]
Ограничения типов (bounds)
# Upper bound — T must implement specific protocol
def maximum[T: (int, float)](values: list[T]) -> T:
"""Return the maximum value."""
if not values:
raise ValueError("Empty list")
return max(values)
maximum([1, 2, 3]) # OK
maximum([1.5, 2.5]) # OK
# maximum(["a", "b"]) # Error: str not in (int, float)
# Bound to a class — T must be subclass of Comparable
from typing import Protocol
class SupportsLessThan(Protocol):
def __lt__(self, other: object) -> bool: ...
def min_val[T: SupportsLessThan](a: T, b: T) -> T:
return a if a < b else b
Обобщенные классы
Старый синтаксис (Generic[T])
from typing import Generic, TypeVar
T = TypeVar("T")
class Stack(Generic[T]):
"""Type-safe stack implementation."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError("Stack is empty")
return self._items.pop()
def peek(self) -> T:
if not self._items:
raise IndexError("Stack is empty")
return self._items[-1]
def __len__(self) -> int:
return len(self._items)
# Usage — type checker tracks T
int_stack: Stack[int] = Stack()
int_stack.push(42)
value: int = int_stack.pop()
str_stack: Stack[str] = Stack()
str_stack.push("hello")
# str_stack.push(42) # Error: expected str, got int
Новый синтаксис Python 3.12+
# Clean syntax — no imports needed
class Stack[T]:
"""Type-safe stack with modern syntax."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError("Stack is empty")
return self._items.pop()
def is_empty(self) -> bool:
return len(self._items) == 0
# Multiple type parameters
class Pair[T, U]:
"""A pair of two values with different types."""
def __init__(self, first: T, second: U) -> None:
self.first = first
self.second = second
def swap(self) -> "Pair[U, T]":
return Pair(self.second, self.first)
def map_first[V](self, func: "Callable[[T], V]") -> "Pair[V, U]":
return Pair(func(self.first), self.second)
pair = Pair(42, "hello")
# pair.first: int, pair.second: str
swapped = pair.swap()
# swapped.first: str, swapped.second: int
Оператор type (Python 3.12+)
Оператор type создает явные псевдонимы типов:
# type statement — Python 3.12+
type Vector = list[float]
type Matrix = list[Vector]
type Point = tuple[float, float]
# Generic type aliases
type ListOf[T] = list[T]
type Pair[T, U] = tuple[T, U]
type Callback[T] = Callable[[T], None]
# Recursive types — now possible!
type JSON = str | int | float | bool | None | list[JSON] | dict[str, JSON]
def parse_json(data: JSON) -> str:
"""Process any valid JSON value."""
match data:
case str():
return f"string: {data}"
case int() | float():
return f"number: {data}"
case bool():
return f"boolean: {data}"
case None:
return "null"
case list():
return f"array of {len(data)} items"
case dict():
return f"object with keys: {list(data.keys())}"
Сравнение с TypeAlias
# Before Python 3.12
from typing import TypeAlias
Vector: TypeAlias = list[float]
# or
Vector = list[float] # Less explicit
# Python 3.12+ — clear and powerful
type Vector = list[float]
type Tree[T] = T | list["Tree[T]"] # Recursive generic alias!
ParamSpec -- generics для сигнатур функций
ParamSpec параметризует сигнатуру функции целиком. Необходим для декораторов:
from typing import ParamSpec, TypeVar, Callable
from functools import wraps
import time
P = ParamSpec("P")
R = TypeVar("R")
def timer(func: Callable[P, R]) -> Callable[P, R]:
"""Decorator that measures execution time."""
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def calculate(x: int, y: int, power: int = 2) -> float:
return (x + y) ** power
# Type checker preserves the original signature
result: float = calculate(3, 4, power=3)
# calculate(3, "4") # Error: expected int
Декоратор retry с ParamSpec
from typing import ParamSpec, TypeVar, Callable
from functools import wraps
P = ParamSpec("P")
R = TypeVar("R")
def retry(
max_attempts: int = 3,
exceptions: tuple[type[Exception], ...] = (Exception,),
) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Retry decorator that preserves function signature."""
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
last_error: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_error = e
print(f"Attempt {attempt} failed: {e}")
raise last_error # type: ignore[misc]
return wrapper
return decorator
@retry(max_attempts=3, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url: str, timeout: float = 30.0) -> dict:
"""Fetch data from URL."""
return {"url": url, "data": "..."}
# Signature preserved — type checker knows parameters
data = fetch_data("https://api.example.com", timeout=10.0)
TypeVarTuple (Python 3.11+)
Для переменного количества типовых параметров:
from typing import TypeVarTuple, Unpack
Ts = TypeVarTuple("Ts")
def first_of[*Ts](*args: *Ts) -> tuple[*Ts]:
"""Return all arguments as a tuple (type-safe)."""
return args
result = first_of(1, "hello", 3.14)
# result: tuple[int, str, float]
Практический пример: обобщенный Repository
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
@dataclass
class Product:
id: int
title: str
price: float
class Repository[T]:
"""Generic in-memory repository for any entity type."""
def __init__(self) -> None:
self._store: dict[int, T] = {}
self._next_id: int = 1
def add(self, entity: T) -> int:
"""Add entity and return its ID."""
entity_id = self._next_id
self._store[entity_id] = entity
self._next_id += 1
return entity_id
def get(self, entity_id: int) -> T | None:
"""Get entity by ID."""
return self._store.get(entity_id)
def all(self) -> list[T]:
"""Return all entities."""
return list(self._store.values())
def delete(self, entity_id: int) -> bool:
"""Delete entity by ID."""
return self._store.pop(entity_id, None) is not None
# Fully type-safe
user_repo: Repository[User] = Repository()
user_repo.add(User(id=1, name="Alice"))
user: User | None = user_repo.get(1)
product_repo: Repository[Product] = Repository()
product_repo.add(Product(id=1, title="Laptop", price=999.99))
product: Product | None = product_repo.get(1)
# product_repo.add(User(id=2, name="Bob")) # Error: expected Product