HardТеория6 min

Generics и Python 3.12+

Обобщенные типы: TypeVar, синтаксис def func[T](), оператор type и ParamSpec

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

Проверь себя

Чем TypeVar с bound отличается от TypeVar с ограничениями?

Что делает оператор type в Python 3.12+?

Какой тип рекурсивного JSON-значения стал возможен благодаря оператору type?

Для чего используется ParamSpec?

Как определить обобщенную функцию в Python 3.12+?