Перейти к содержанию

Шпаргалка по протоколам

Утиная типизация и протоколы

Утиная типизация — это неявная реализация интерфейса через реализацию набора специальных методов. Объект, который их реализует, имитирует работу определенного типа без явного наследования интерфейса.

Python является языком протоколов. Суть протоколов в том, что помимо специальных методов, реализующих интерфейс они включают другие виды взаимодействий. Например, генерация специальных исключений.

Протоколы

Протокол — это способ реализовать неявный интерфейс без дополнительных средств.

Протокол сопоставления (Comparable)

  • Если метод eq() не переопределен, он возвращает 'id(self) == id(other)', что то же самое, что и 'self is other'.
  • Это означает, что по умолчанию все объекты не идентичны.
  • Метод сравнения вызывается только у левого объекта. Но если он вернет NotImplemented, то метод сравнения вызывается у правого объекта.
  • __ne__() автоматически работает на любом объекте, для которого определен метод __eq__().
class MyComparable:
    def __init__(self, a):
        self.a = a
    def __eq__(self, other):
        if isinstance(other, type(self)):
            return self.a == other.a
        return NotImplemented

Хэширование объектов (Hashable)

  • Хешируемому объекту нужны __hash()__ и __eq()__, и для объекта значение его хэша никогда не должно меняться
  • Хешируемые объекты считающиеся равными должны иметь одинаковые значения хеша. Например, __hash()__ которая возвращает 'id(self)' работать не будет
  • Поэтому Python автоматически делает классы не хешируемыми если будет реализован только метод __eq__()
class MyHashable:
    def __init__(self, a):
        self._a = a
    @property
    def a(self):
        return self._a
    def __eq__(self, other):
        if isinstance(other, type(self)):
            return self.a == other.a
        return NotImplemented
    def __hash__(self):
        return hash(self.a)

Сортируемые (Sortable)

  • При использовании декоратора 'total_ordering' вам нужно предоставить только __eq__() и один из специальных методов __lt__(), __gt__(), __le__() или __ge__(), остальное будет сгенерировано автоматически
  • Функции sorted() и min() требуют только метода __lt__(), а max() - только __gt__(). Однако лучше определить их все, чтобы не возникало путаницы в других контекстах
  • При сравнении двух списков, строк или dataclass значения сравниваются по порядку, пока не будет найдена пара неравных значений. Результат сравнения возвращается. Если сравниваются последовательности, то более короткая считается меньшей в случае, если все значения равны.
from functools import total_ordering

@total_ordering
class MySortable:
    def __init__(self, a):
        self.a = a
    def __eq__(self, other):
        if isinstance(other, type(self)):
            return self.a == other.a
        return NotImplemented
    def __lt__(self, other):
        if isinstance(other, type(self)):
            return self.a < other.a
        return NotImplemented

Итератор (Iterator)

  • Любой объект, у которого есть методы __next__() и __iter__() являются итераторами
  • Метод __next__() должен вернуть следующий элемент или вызвать StopIteration
  • Метод __iter__() должен возвращаться 'self'
class Counter:
    def __init__(self):
        self.i = 0
    def __next__(self):
        self.i += 1
        return self.i
    def __iter__(self):
        return self
>>> counter = Counter()
>>> next(counter), next(counter), next(counter)
(1, 2, 3)

В Python много разных типов объектов итераторов:

  • Последовательности, которые возвращаются функцией iter(), например list_iterator и set_iterator
  • Объекты, которые возвращаются функциями модуля itertools, например count, repeat и cycle
  • Генераторы, создаваемые функциями генераторами (с ключевым слово yield) и генераторными выражениями
  • Файловыми объектами, которые получаются функцией open() и другими

Вызываемый тип (Callable)

  • Все функции и классы, у которых есть метод __call__() считаются вызываемыми (callable)
  • Когда эта шпаргалка использует '<function>' в качестве аргумента, это означает '<callable>'
class Counter:
    def __init__(self):
        self.i = 0
    def __call__(self):
        self.i += 1
        return self.i
>>> counter = Counter()
>>> counter(), counter(), counter()
(1, 2, 3)

Контекстный менеджер (Context Manager)

  • Метод __enter__() должен заблокировать необходимые ресурсы и, возможно, вернуть объект
  • Метод __exit__() должен освободить заблокированные ресурсы
  • Любое исключение, которое случается внутри блока with передается методу __exit__()
  • Если исключение должно быть подавлено, то метод __exit__() должен вернуть значение True
class MyOpen:
    def __init__(self, filename):
        self.filename = filename
    def __enter__(self):
        self.file = open(self.filename)
        return self.file
    def __exit__(self, exc_type, exception, traceback):
        self.file.close()
>>> with open('test.txt', 'w') as file:
...     file.write('Hello World!')
>>> with MyOpen('test.txt') as file:
...     print(file.read())
Hello World!

Утиная типизация для итераторов

Итерируемый (Iterable)

  • Единственный необходимый метод - __iter__(). Он должен возвращать итератор элементов объекта
  • __contains__() автоматически работает на любом объекте, для которого определен метод __iter__()
class MyIterable:
    def __init__(self, a):
        self.a = a
    def __iter__(self):
        return iter(self.a)
    def __contains__(self, el):
        return el in self.a
>>> obj = MyIterable([1, 2, 3])
>>> [el for el in obj]
[1, 2, 3]
>>> 1 in obj
True

Коллекции (Collection)

  • Обязательные методы __iter__() и __len__()
  • Если вам в документации попадались описания функций или методов, которые получают на вход коллекции (термин collection) то может возникнуть впечатление, что они так же могут работать и с итерируемыми объектами. Но есть случаи, даже в стандартной библиотеке, когда это не так. Поэтому эти термины не равнозначны.
class MyCollection:
    def __init__(self, a):
        self.a = a
    def __iter__(self):
        return iter(self.a)
    def __contains__(self, el):
        return el in self.a
    def __len__(self):
        return len(self.a)

Последовательности (Sequence)

  • Обязательные методы __len__() и __getitem__()
  • Метод __getitem__() должен вернуть элемент под нужным индексом или породить IndexError
  • Методы __iter__() и __contains__() автоматом работают с любым объектом, у которого определен 'getitem()'
  • Функция reversed() автоматически работает с любым объектом, у которого есть методы __len__() и __getitem__()
class MySequence:
    def __init__(self, a):
        self.a = a
    def __iter__(self):
        return iter(self.a)
    def __contains__(self, el):
        return el in self.a
    def __len__(self):
        return len(self.a)
    def __getitem__(self, i):
        return self.a[i]
    def __reversed__(self):
        return reversed(self.a)

Таблица методов

Таблица необходимых и автоматически доступных специальных методов:

Iterable Collection Sequence
__iter__() REQ REQ Yes
__contains__() Yes Yes Yes
__len__() REQ REQ
__getitem__() REQ
__reversed__() Yes
__index__()
__count__()