Программирование
August 8, 2023

Протоколы в python

Вместо предисловия

Листая хабр на предмет интересных статеек наткнулся на текст про протоколы в python. Эта заметка в меру корявенькая и некоторые вопросы рассматривает не совсем верно (а какие-то нюансы и вовсе не затрагивает), но спасибо ей, что побудила меня сесть и поразбираться в том, как это работает в действительности. Более удачная статейка с хабра по теме, если кому интересно.

Утиная типизация

Да кто такой этот ваш протокол?

К своему стыду, я понятия не имел о том, что протоколы в python существуют и можно больше не городить цепочку абстрактных классов для достижения более привычной типизации (в сравнении с другими языками). Стоит отметить также, что чаще можно встретить понятие interface, а не protocol, но по сути оба этих подхода позволяют нам определить некий контракт для классов, который они должны будут реализовать.

Установка контракта для класса необходима для достижения нескольких целей:

  • регламентация набора атрибутов и методов;
  • гарантия соответствия всех дочерних классов общей структуре, установленной контрактом.

Один из моих любимых вопросов для собеседований как раз касается смежной темы, — Dependency Injection — принцип из набора SOLID, реализация которого без таких контрактов довольно затруднительна.

Резюмируя: протокол — это контракт для класса, регламентирующий набор атрибутов и методов, которому должны будут следовать все классы, реализующие этот контракт.

Важные нюансы из спецификации

В спецификации рассматриваются и другие существующие подходы к структурной подтипизации. В том числе, там есть и возможность использования interface из библиотеки zope.interface.

Что касается множественного наследования от нескольких протоколов и обычных классов:

"A class can explicitly inherit from multiple protocols and also from normal classes. In this case methods are resolved using normal MRO and a type checker verifies that all subtyping are correct. The semantics of @abstractmethod is not changed, all of them must be implemented by an explicit subclass before it can be instantiated." — то есть, в сущности ничего для множественного наследования не меняется, мы можем опираться на прежние правила при использовании такого подхода.

За это стоит сказать отдельное спасибо разработчикам python, потому что не появляется дополнительной вещи, которую нужно держать в голове, можно просто опираться на привычный MRO.

Ещё одна важная деталь, которой стоит уделить внимание — это работа методов isinstance(), issubclass():

"The default semantics is that isinstance() and issubclass() fail for protocol types. This is in the spirit of duck typing — protocols basically would be used to model duck typing statically, not explicitly at runtime." — то есть, isinstance() и issubclass() не будут работать для протоколов по умолчанию, для этого надо использовать специальный декоратор @runtime_checkable, пример его использования:

from typing import runtime_checkable, Protocol

@runtime_checkable
class SupportsClose(Protocol):
    def close(self):
        ...

assert isinstance(open('some/file'), SupportsClose)

К тому же, в спецификации говорится следующее:

"isinstance() can be used with both data and non-data protocols, while issubclass() can be used only with non-data protocols. This restriction exists because some data attributes can be set on an instance in constructor and this information is not always available on the class object.", — по сути, это ограничение на использование метода issubclass() для non-data protocol (non-data protocol - это протокол, членами которого являются только методы, если же протокол содержит хотя бы один член, который не является методом, он именуется data protocol). Обосновывается такое ограничение тем, что часть атрибутов может быть установлена внутри конструктора и такая информация не всегда доступна для экземпляра класса.

Заключение

Забавно наблюдать, как языки с динамической типизацией стремятся реализовать всё больше инструментов для использования возможностей статической типизации. Несомненным плюсом во всей этой ситуации является отсутствие обязательности использования подобных возможностей языка. Однако, аннотация типов и использование абстрактных классов уже давно являются негласным признаком хорошего тона в сообществе python-разработчиков. А разработку на JS без использования TS и вовсе пытаются занести в список смертных грехов, но это уже тема для отдельной заметки.