PythonMiddleTechnical

Чем typing.Protocol отличается от ABC и когда structural typing полезнее наследования?

Protocol реализует structural typing (duck typing со статической проверкой) — класс соответствует протоколу, если у него есть нужные методы, без явного наследования. ABC требует наследования и удобен, когда нужна общая реализация или явная иерархия.

typing.Protocol против ABC: structural typing в Python

В Python есть два способа выразить контракт типа: номинальная типизация (ABC + наследование — «является ли объект подклассом X?») и структурная типизация (Protocol — «имеет ли объект нужные атрибуты/методы?»). Protocol реализует duck typing с проверкой статическим анализатором.

ABC: номинальная типизация

from abc import ABC, abstractmethod

class Drawable(ABC):
    @abstractmethod
    def draw(self) -> None: ...

    @abstractmethod
    def resize(self, factor: float) -> None: ...

class Circle(Drawable):  # явное наследование ОБЯЗАТЕЛЬНО
    def draw(self) -> None:
        print("circle")

    def resize(self, factor: float) -> None:
        self.radius *= factor

def render(shape: Drawable) -> None:
    shape.draw()

Если класс не наследует Drawable, mypy выдаст ошибку, даже если у него есть методы draw и resize.

Protocol: структурная типизация

from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None: ...
    def resize(self, factor: float) -> None: ...

class Circle:  # НЕТ наследования от Drawable
    def draw(self) -> None:
        print("circle")

    def resize(self, factor: float) -> None:
        self.radius *= factor

class SVGElement:  # тоже нет наследования
    def draw(self) -> None:
        print("svg")

    def resize(self, factor: float) -> None:
        pass

def render(shape: Drawable) -> None:  # работает с любым классом
    shape.draw()

render(Circle())      # OK
render(SVGElement())  # OK

# runtime_checkable позволяет isinstance
print(isinstance(Circle(), Drawable))  # True

Когда Protocol полезнее ABC

  • Сторонние библиотеки: нельзя изменить класс из внешней библиотеки, чтобы он наследовал ваш ABC. С Protocol он соответствует контракту автоматически.
  • Несвязанные домены: pathlib.Path и io.BytesIO оба имеют метод read() — можно создать Protocol Readable без изменения стандартной библиотеки.
  • Тестирование: mock-объекты и стабы автоматически проходят проверку, если реализуют нужные методы.

Когда ABC лучше Protocol

  • Нужна общая реализация по умолчанию (@property с базовой логикой).
  • Важна явная иерархия — например, все BaseModel в pydantic должны явно наследоваться.
  • Runtime-проверки isinstance критичны для работы кода (не только для mypy).

Protocol с атрибутами и ClassVar

from typing import Protocol, ClassVar

class Configurable(Protocol):
    config_key: ClassVar[str]  # атрибут класса
    name: str                  # атрибут экземпляра

    def validate(self) -> bool: ...

class DatabaseConfig:
    config_key = "database"  # ClassVar
    name: str

    def __init__(self, name: str):
        self.name = name

    def validate(self) -> bool:
        return bool(self.name)

Расширение Protocol через наследование

from typing import Protocol

class Reader(Protocol):
    def read(self, n: int = -1) -> bytes: ...

class Writer(Protocol):
    def write(self, data: bytes) -> int: ...

class ReadWriter(Reader, Writer, Protocol):  # комбинирование
    ...

Подводные камни

  • @runtime_checkable проверяет только наличие методов, но не их сигнатуры. isinstance(obj, MyProtocol) вернёт True, даже если метод принимает другие аргументы.
  • Protocol не поддерживает super() — если нужна реализация по умолчанию, используйте mixin или ABC.
  • Приватные методы (с __prefix) в Protocol не работают корректно из-за name mangling — используйте одинарное подчёркивание.
  • mypy и pyright могут по-разному трактовать Protocol с атрибутами, у которых нет аннотаций типов в реализующем классе.
  • Производительность isinstance с @runtime_checkable линейна от числа членов Protocol — не используйте в горячем пути.
  • Protocol с методами, возвращающими Self, требует from typing import Self (Python 3.11+) — в старых версиях нужен TypeVar.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics