Когда следует использовать `Protocol` для определения интерфейсов в Python?

Ответ

Протоколы (typing.Protocol) в Python используются для определения интерфейсов на основе структурной типизации (также известной как 'утиная типизация'). Они позволяют статическим анализаторам типов (например, mypy, pyright) проверять, соответствует ли объект определенному набору методов и атрибутов, без необходимости явного наследования.

Основные сценарии использования Protocol:

  1. Абстракция без наследования: Когда нужно определить интерфейс, но нет необходимости или возможности использовать абстрактные базовые классы (abc.ABC) или явное наследование. Это идеально для случаев, когда классы уже существуют и реализуют нужные методы, но не имеют общего предка.

    from typing import Protocol
    
    class Drawable(Protocol):
        def draw(self) -> None: ... # Определение сигнатуры метода
    
    class Circle:
        def draw(self) -> None:
            print("Drawing a circle")
    
    class Square:
        def draw(self) -> None:
            print("Drawing a square")
    
    def render(obj: Drawable) -> None:
        obj.draw()
    
    render(Circle()) # Работает, так как Circle соответствует протоколу Drawable
    render(Square())
  2. Обратная совместимость и интеграция: Для типизации объектов из сторонних библиотек или унаследованного кода, где вы не можете изменить иерархию наследования, но хотите обеспечить проверку типов.

  3. Аннотация callback-функций: Для четкого описания ожидаемой сигнатуры функций, передаваемых в качестве аргументов.

    from typing import Protocol
    
    class StringProcessor(Protocol):
        def __call__(self, data: str) -> int: ...
    
    def process_data(processor: StringProcessor, text: str) -> int:
        return processor(text)
    
    def my_processor(s: str) -> int:
        return len(s)
    
    print(process_data(my_processor, "hello")) # 5
  4. Гибкость в дизайне: Позволяют создавать более гибкие и слабосвязанные системы, где компоненты взаимодействуют через их поведение, а не через их иерархию классов.

Протоколы особенно полезны в сочетании с декоратором @runtime_checkable (из typing), который позволяет проверять соответствие протоколу во время выполнения с помощью isinstance() и issubclass(), что расширяет их применение за пределы статической проверки типов.