class Settings:
def __init__(self):
self._data = {}
def add_property(self, key, value):
assert isinstance(key, str), key
self._data[key] = value
def get_property(self, key):
assert isinstance(key, str), key
return self._data[key]
И тут нам в голову приходит удачная идея, что было бы здорово вместо вызова settings.get_property('key') использовать квадратные скобки как для dict: settings['key']:def __getitem__(self, key):
return self.get_property(key)
Что не так?То, что наш класс стал отчасти походить на readonly dict (он же mapping) -- но он не реализует весь предполагаемый контракт.
Так, я привык, что если класс похож на readonly dict, то он позволяет узнать количество элементов в нём. Добавляем
__len__:def __len__(self):
return len(self._data)
Всё ещё не хорошо. Для mapping обычно можно итерироваться по ключам. Добавление __iter__ решает проблему:def __iter__(self):
return iter(self._data)
Всё? Нет! Хочется ещё проверять на наличие ключа: key in settings -- dict ведь это позволяет!Можем добавить метод
__contains__ -- а можем вспомнить, что есть класс collections.abc.Mapping.Это абстрактный базовый класс, задающий контракт для неизменяемого словаря.
Описание того, что таке абстрактный базовый класс -- здесь
Просто наследуемся от
Mapping:from collections.abc import Mapping
class Settings(Mapping):
# ...
В качестве бесплатного бонуса получам поддержку .get(), .keys(), .items(), .values(), __eq__ и __ne__.Реализация этих методов не оптимальная с точки зрения производительности, но она уже есть из коробки. К тому же всегда можно добавть свой вариант, который будет работать быстрее стандартного (если мы знаем как это сделать).
Если мы забудем реализовать какой-то критически важный метод -- при создании экземпляра класса получим исключение:
>>> settings = Settings()
TypeError: Can't instantiate abstract class Settings with abstract methods __iter__
В стандартной библиотеке есть большой набор абстрактных базовых классов:- ByteString
- Callable
- Container
- Hashable
- ItemsView
- Iterable
- Iterator
- KeysView
- Mapping
- MappingView
- MutableMapping
- MutableSequence
- MutableSet
- Sequence
- Set
- Sized
- ValuesView
При необходимаости можно (и нужно) написать свои.
А в заключение забавный пример.
В библиотеке
sqlalchemy есть класс RowProxy для строки-кортежа, получаемой в результате SQL запроса.Класс выглядит как mapping: имеет длину,
.keys(), .items(), .__contains__() и все прочие нужные методы. Позволяет получать значение как по позиционному номеру так и по названию колонки в базе данных.При этом он реализует контракт
Sequence (как у tuple).Т.е.
iter(row) возвращает данные, а не названия колонок. И это немного сбивает с толку: выглядит как утка, а крякает как поросёнок.В оправдание
sqlalchemy могу сказать, что RowProxy появился в самой первой версии алхимии, еще до того как в Питон добавили collections.abc. А потом что-то менять стало поздно.Но сейчас при разработке собственных библиотек стоит придерживаться устоявшихся стандартов и активно применять абстрактные базовые классы для коллекций.