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()— можно создать ProtocolReadableбез изменения стандартной библиотеки. - Тестирование: 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.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.