Аннотации типов, 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, и на практике он нужен редко.
Проверьте себя
1. Что означает аннотация Optional[str]?
AСтрока обязательна
BСтрока или None
CСписок строк
DЛюбой тип, кроме строки
2. Проверяет ли Python аннотации типов во время выполнения?
AДа, и бросает ошибку при несоответствии
BНет, они игнорируются в рантайме и нужны инструментам (IDE, линтерам)
CТолько в классах
DТолько для аргументов функций
3. Что такое метакласс?
AКласс, у которого нет методов
BКласс, экземплярами которого являются другие классы (по умолчанию type)
CКласс для работы с метаданными файлов
DСиноним абстрактного класса
Поддержать проект