На этом шаге мы рассмотрим использование модуля collections.
Вы хотите реализовать собственный кастомный класс, который копирует поведение обычного встроенного типа контейнера, такого как список или словарь. Однако вы не полностью уверены, что знаете, какие методы нужно реализовать.
Библиотека collections определяет разнообразные абстрактные базовые классы, которые чрезвычайно полезны при реализации собственных классов контейнеров. Для примера предположим, что вы хотите создать класс с поддержкой итераций. Чтобы сделать это, унаследуйте его от collections.Iterable, как показано тут:
>>> import collections >>> class A(collections.Iterable): pass
Наследование от collections.Iterable проверяет, что вы реализовали все требуемые специальные методы. Если вы не сделаете этого, то получите ошибку при создании экземпляра:
>>> a = A()
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
a = A()
TypeError: Can't instantiate abstract class A with abstract methods __iter__
>>>
Чтобы исправить эту ошибку, просто дайте классу требуемый метод __iter__() и реализуйте его так, как хотите (см. шаги 63 и 67).
Другие важные классы,определенные в collections, - это Sequence, MutableSequence, Mapping, MutableMapping, Set и MutableSet. Многие из этих классов формируют иерархии с увеличивающими уровнями функциональности (одна из таких иерархий - Container, Iterable, Sized, Sequence и MutableSequence). Еще раз: просто создайте экземпляр любого из этих классов, чтобы увидеть, какие методы нужны, чтобы реализовать собственный контейнер с требуемым поведением:
>>> import collections >>> collections.Sequence() Traceback (most recent call last): File "<pyshell#6>", line 1, in <module> collections.Sequence() TypeError: Can't instantiate abstract class Sequence with abstract methods __getitem__, __len__ >>>
Вот простой пример класса, который реализует предшествующие методы, чтобы создать последовательность, в которой элементы хранятся в отсортированном порядке (это не самая эффективная реализация, но она иллюстрирует общую идею):
>>> import collections >>> import bisect >>> class SortedItems(collections.Sequence): def __init__(self, initial=None): self._items = sorted(initial) if initial is not None else [] # Требуемые методы последовательности def __getitem__(self, index): return self._items[index] def __len__(self): return len(self._items) # Метод для добавления элемента в правильное место def add(self, item): bisect.insort(self._items, item) >>>
Вот пример использования этого класса:
>>> items = SortedItems([5, 1, 3]) >>> list(items) [1, 3, 5] >>> items[0] 1 >>> items[-1] 5 >>> items.add(2) >>> list(items) [1, 2, 3, 5] >>> items.add(-10) >>> list(items) [-10, 1, 2, 3, 5] >>> items[1:4] [1, 2, 3] >>> 3 in items True >>> len(items) 5 >>> for n in items: print(n) -10 1 2 3 5 >>>
Как вы можете видеть, экземпляры SortedItems ведут себя в точности как обычная последовательность и поддерживают все обычные операции, включая индексирование, итерирование, len(), проверку на содержание (оператор in) и даже извлечение срезов.
А вот модуль bisect, использованный в этом рецепте, дает удобный способ поддерживать отсортированность элементов в списке. Поскольку
bisect.insort() вставляет элемент в список, последовательность остается отсортированной.
Наследование от одного из абстрактных базовых классов из collections позволяет удостовериться, что ваш собственный контейнер реализует все требуемые методы, которые нужны контейнеру. Также наследование упрощает проверку типов. Например, ваш собственный контейнер пройдет проверки типов:
>>> items = SortedItems() >>> import collections >>> isinstance(items, collections.Iterable) True >>> isinstance(items, collections.Sequence) True >>> isinstance(items, collections.Container) True >>> isinstance(items, collections.Sized) True >>> isinstance(items, collections.Mapping) False >>>
Многие абстрактные базовые классы из collections также предоставляют дефолтные реализации обычных методов контейнеров. Предположим, например, что у вас есть класс, который наследует от collections.MutableSequence:
>>> class Items(collections.MutableSequence): def __init__(self, initial=None): self._items = list(initial) if initial is not None else [] # Required sequence methods def __getitem__(self, index): print('Getting:', index) return self._items[index] def __setitem__(self, index, value): print('Setting:', index, value) self._items[index] = value def __delitem__(self, index): print('Deleting:', index) del self._items[index] def insert(self, index, value): print('Inserting:', index, value) self._items.insert(index, value) def __len__(self): print('Len') return len(self._items) >>>
Если вы создадите экземпляр Items, то обнаружите, что он поддерживает практически все основные методы (например, append(), remove(), count() и т. д.). Эти методы реализованы таким образом, что они используют только требуемые методы. Вот интерактивный сеанс, который демонстрирует это:
>>> a = Items([1, 2, 3]) >>> len(a) Len 3 >>> a.append(4) Len Inserting: 3 4 >>> a.append(2) Len Inserting: 4 2 >>> a.count(2) Getting: 0 Getting: 1 Getting: 2 Getting: 3 Getting: 4 Getting: 5 2 >>> a.remove(3) Getting: 0 Getting: 1 Getting: 2 Deleting: 2 >>>
Этот рецепт - лишь небольшой экскурс в функциональность абстрактных базовых классов Python. Модуль numbers предоставляет похожую коллекцию абстрактных классов, связанных с числовыми типами данных.
На следующем шаге мы рассмотрим делегирование доступа к атрибуту.