Сегодня у нас немного относительно сложного материальчика, который в 2024 году становится очень популярным, т.к. классическое ООП во всю сдает свои позиции.
Все, о чем будет разговор в этой заметке будет посвещено типизации в Python, обрабатываемой с помощью статических анализаторов, а в частности, mypy.
И начнем мы с того, что типы в Python бывают как минимум двух видов: номинальные и структурные.
Номинальный (nominal) тип -- набор значений (читай, объектов), чей атрибут __class__
является транзитивным типом или любым его подклассом.
Если не знаете или не помните из школы, нагуглите, что такое транзитивность.
Структурный (structural) тип -- набор значений, определяемых не магическим атрибутом __class__
, а по их свойствам: атрибутам, методам, ключам и т.п.
И разговор сегодня пойдет про то, как же контролировать структурные типы. А занимается этим его величество класс Protocol
из библиотеки typing
, который является инстансом ABCMeta. Если интересно, гляньте на реализацию, станет понятно, как cpython определяет является ли класс протоколом и как такие классы взаимодействуют с isinstance()
. А можно просто прочитать PEP544.
Определяется протокол с помощью наследования от класса Protocol
.
class ConvertableProtocol(Protocol):
def convert(self) -> None:
...
Таким нехитрым способом был объявлен протокол (кто знает ООП, то читай, интерфейс), говорящий, что в любом объекте, который будет следовать этому протоколу должен быть метод convert()
с указанной сигнатурой.
Сопру определние с сайта.
Сигнатура метода - это его уникальная идентификационная схема, которая включает в себя имя метода, типы его параметров (их порядок и количество) и тип возвращаемого значения.
Пусть у нас теперь есть два таких класса и некая функция, которая в параметре принимает объект, следующий объявленному ранее протоколу.
class JPGFile:
def convert(self) -> None:
print('jpg converted')
class BMPFile:
def convert_file(self):
print('bmp converted')
def convert_obj(obj: ConvertableProtocol):
obj.convert()
В одном классе есть метод, названный так же, как в протоколе, в другом -- названный по-другому.
Дальше понадобится mypy (хотя PyCharm справляется и сам). Добавляем вызовы функции с объектами разных классов.
convert_obj(JPGFile())
convert_obj(BMPFile())
И скармливаем все это дело mypy:
Argument 1 to "convert_obj" has incompatible type "BMPFile"; expected "ConvertableProtocol"
Класс, который не наследует напрямую протокол, соответствует типу этого протокола. Магия, не иначе.
Протоколы можно использовать в нормальных типах. Пусть, есть функция, которая должна принимать какой-то контейнер итерируемый, все объекты внутри которого должны следовать протоколу выше. И сразу вызовем функцию с передачей списка из "правильного" и "неправильного" объектов.
from typing import Iterable
...
def convert_files(files: Iterable[ConvertableProtocol]):
for file in files:
convert_obj(file)
...
convert_files(
[JPGFile(), BMPFile()]
)
Проверка mypy снова показывает магию!
List item 1 has incompatible type "BMPFile"; expected "ConvertableProtocol"
Ну а дальше тут все очень похоже на привычный ООП, только часто можно обойстись без огромной цепочки наследования. Например, можно внутри протокола использовать абстрактные, классовые, статические методы.
class ConvertableProtocol(Protocol):
def convert(self) -> None:
...
@abstractmethod
def method(self):
raise NotImplementedError
Попробуйте запустить код, посмотрите на сообщения.
Внутри протокола можно использовать переменные класса, но их придется аннотировать с помощью ClassVar.
Протоколы можно наследовать от других протоколов, таким образом собирая нужный интерфейс из кусков. Пусть объекты нашего протокола, допускающего конвертацию объектов, должны еще поддерживать функцию len()
. Просто пишем в наследниках еще и тип Sized.
class ConvertableProtocol(Sized, Protocol):
...
Теперь во всех классах, которые должны следовать этому протоколу придется описать метод __len__()
.
За кадром остались обобщенные и рекурсивные протоколы. Но я не смог придумать легкий пример, демонстрирующий их приколы. Поэтому отправлю читать доку. Изучите сначала, что такое ковариантность, контравариантность и инвариантность в программировании.
Резюме такое. Этот подход точно будет дальше активно лезть в продакшн, этот подход сложнее, python может стать практически на с++. Использовать или нет -- выбор каждого. Я точно буду, мне зашло.