Аннотации типов, typing и метаклассы
Как аннотации типов делают код понятнее, что дают Optional, Generic и Protocol, и что такое метакласс.
Аннотации типов — необязательные подсказки о типах переменных, аргументов и возвращаемых значений; Python их не проверяет в рантайме, но их используют редакторы и линтеры.
Optional: «значение или ничего»
Аннотации не влияют на выполнение, но делают намерения явными. Optional[str] означает «строка или None» — частый случай для необязательных параметров.
from typing import Optional
def greet(name: Optional[str] = None) -> str:
if name is None:
return "Привет, гость!"
return f"Привет, {name}!"
print(greet())
print(greet("Лена"))
Вывод:
Привет, гость! Привет, Лена!
Аннотация -> str сообщает, что функция возвращает строку. Python не заставит её это делать, но IDE подскажет ошибку, если вы попытаетесь вернуть число.
Generic: типобезопасные контейнеры
Generic и TypeVar позволяют описать контейнер, работающий с любым типом, сохраняя информацию о нём. Box[int] и Box[str] — один класс, но инструменты знают, что внутри.
from typing import TypeVar, Generic
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, item: T):
self.item = item
def get(self) -> T:
return self.item
b_int = Box(42)
b_str = Box("привет")
print(b_int.get())
print(b_str.get())
Вывод:
42 привет
Protocol: утиная типизация по сигнатуре
Protocol описывает требуемое поведение, а не конкретный класс: «подойдёт любой объект, у которого есть нужные методы». Это формализация «утиной типизации» — если объект умеет нужное, он подходит, наследование не требуется.
from typing import Protocol
class Comparable(Protocol):
def __lt__(self, other) -> bool: ...
def maximum(items):
best = items[0]
for x in items[1:]:
if best < x:
best = x
return best
print(maximum([3, 1, 4, 1, 5, 9, 2]))
print(maximum(["яблоко", "банан", "апельсин"]))
Вывод:
9 яблоко
Функция maximum работает и с числами, и со строками — обоим типам доступен оператор <. Protocol формально фиксирует это требование («объект должен поддерживать сравнение»), не привязываясь к конкретному классу.
Метаклассы: кратко
Финальная тема — на уровне идеи. Если класс — это «фабрика», создающая объекты, то метакласс — это «фабрика классов», то есть класс, экземплярами которого являются сами классы. По умолчанию все классы создаются метаклассом type.
class A:
pass
# тип объекта — его класс; тип класса — его метакласс
print("Тип объекта A():", type(A()).__name__)
print("Метакласс класса A:", type(A).__name__)
print("Метакласс int:", type(int).__name__)
Вывод:
Тип объекта A(): A Метакласс класса A: type Метакласс int: type
Метаклассы позволяют перехватывать создание класса и менять его на лету — например, автоматически регистрировать классы, добавлять методы или проверять структуру. Это мощный, но редко нужный инструмент: на практике почти всегда достаточно декораторов классов или __init_subclass__. Правило здравого смысла: «если вы не уверены, нужен ли вам метакласс, — он вам не нужен».
Итог
- Аннотации типов не проверяются в рантайме, но помогают редакторам и линтерам;
Optional[X]— «X или None». Generic+TypeVarописывают контейнеры, сохраняющие тип содержимого.Protocolформализует утиную типизацию: важны методы, а не наследование.- Метакласс — «класс класса»; по умолчанию это
type, и на практике он нужен редко.