Як я можу зробити максимально «досконалим» підклас дикту?
Кінцевою метою є створення простого диктанту, в якому клавіші є малі.
Якщо я перекрию __getitem__
/ __setitem__
, то get / set не працює. Як змусити їх працювати? Невже мені не потрібно їх реалізовувати окремо?
Чи заважаю соління працювати, і мені потрібно застосовувати
__setstate__
тощо?
Чи потрібно мені перевидання, оновлення та __init__
?
Чи повинен я просто використовувати mutablemapping
(здається, один не повинен використовувати UserDict
або DictMixin
)? Якщо так, то як? Документи не зовсім освічуючі.
Прийнята відповідь була б моїм першим підходом, але оскільки в ній є деякі питання, і оскільки ніхто не звертався до альтернативи, насправді підкласу a dict
, я збираюся це зробити тут.
Що не так у прийнятій відповіді?
Це здається мені досить простим запитом:
Як я можу зробити максимально «досконалим» підклас дикту? Кінцевою метою є створення простого диктанту, в якому клавіші є малі.
Прийнята відповідь насправді не є підкласом dict
, і тест на це не вдається:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
В ідеалі будь-який код перевірки типу буде тестуванням інтерфейсу, який ми очікуємо, або абстрактного базового класу, але якщо наші об’єкти даних переходять у функції, які тестуються, dict
- і ми не можемо "виправити" ці функції, цей код не вдасться.
Інші вигадки, які можна зробити:
- Прийнятий відповідь також відсутній Метод класу:
fromkeys
.
Прийнята відповідь також є зайвою __dict__
- тому займаючи більше місця в пам'яті:
>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
Фактично підкласифікація dict
Ми можемо повторно використовувати методи викладання шляхом успадкування. Все, що нам потрібно зробити, - це створити інтерфейсний шар, який гарантує, що ключі передаються в дикт у малій формі, якщо вони є рядками.
Якщо я перекрию __getitem__
/ __setitem__
, то get / set не працює. Як змусити їх працювати? Невже мені не потрібно їх реалізовувати окремо?
Що ж, їх реалізація окремо є недоліком цього підходу та перевагою використання MutableMapping
(див. Прийняту відповідь), але насправді це не так вже й багато роботи.
Спочатку давайте визначимо різницю між Python 2 та 3, створимо синглтон ( _RaiseKeyError
), щоб переконатися, що ми знаємо, чи насправді отримуємо аргумент dict.pop
, і створимо функцію, щоб переконатися, що наші рядкові клавіші є малі:
from itertools import chain
try: # Python 2
str_base = basestring
items = 'iteritems'
except NameError: # Python 3
str_base = str, bytes, bytearray
items = 'items'
_RaiseKeyError = object() # singleton for no-default behavior
def ensure_lower(maybe_str):
"""dict keys can be any hashable object - only call lower if str"""
return maybe_str.lower() if isinstance(maybe_str, str_base) else maybe_str
Тепер ми реалізуємо - я використовую super
з повними аргументами, щоб цей код працював для Python 2 і 3:
class LowerDict(dict): # dicts take a mapping or iterable as their optional first argument
__slots__ = () # no __dict__ - that would be redundant
@staticmethod # because this doesn't make sense as a global function.
def _process_args(mapping=(), **kwargs):
if hasattr(mapping, items):
mapping = getattr(mapping, items)()
return ((ensure_lower(k), v) for k, v in chain(mapping, getattr(kwargs, items)()))
def __init__(self, mapping=(), **kwargs):
super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
def __getitem__(self, k):
return super(LowerDict, self).__getitem__(ensure_lower(k))
def __setitem__(self, k, v):
return super(LowerDict, self).__setitem__(ensure_lower(k), v)
def __delitem__(self, k):
return super(LowerDict, self).__delitem__(ensure_lower(k))
def get(self, k, default=None):
return super(LowerDict, self).get(ensure_lower(k), default)
def setdefault(self, k, default=None):
return super(LowerDict, self).setdefault(ensure_lower(k), default)
def pop(self, k, v=_RaiseKeyError):
if v is _RaiseKeyError:
return super(LowerDict, self).pop(ensure_lower(k))
return super(LowerDict, self).pop(ensure_lower(k), v)
def update(self, mapping=(), **kwargs):
super(LowerDict, self).update(self._process_args(mapping, **kwargs))
def __contains__(self, k):
return super(LowerDict, self).__contains__(ensure_lower(k))
def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
return type(self)(self)
@classmethod
def fromkeys(cls, keys, v=None):
return super(LowerDict, cls).fromkeys((ensure_lower(k) for k in keys), v)
def __repr__(self):
return '{0}({1})'.format(type(self).__name__, super(LowerDict, self).__repr__())
Ми використовуємо майже шаблонний підхід для будь-якого методу або спеціального методу , який посилається на ключ, але в іншому, у спадок, ми отримуємо методи: len
, clear
, items
, keys
, popitem
, і values
безкоштовно. Хоча для цього потрібна ретельна думка, щоб вийти правильно, банально бачити, що це працює.
(Зауважте, що haskey
це застаріле в Python 2, видалено в Python 3.)
Ось якесь використання:
>>> ld = LowerDict(dict(foo='bar'))
>>> ld['FOO']
'bar'
>>> ld['foo']
'bar'
>>> ld.pop('FoO')
'bar'
>>> ld.setdefault('Foo')
>>> ld
{'foo': None}
>>> ld.get('Bar')
>>> ld.setdefault('Bar')
>>> ld
{'bar': None, 'foo': None}
>>> ld.popitem()
('bar', None)
Чи заважаю соління працювати, і мені потрібно застосовувати
__setstate__
тощо?
маринування
І соління підкласу Дікт просто чудово:
>>> import pickle
>>> pickle.dumps(ld)
b'\x80\x03c__main__\nLowerDict\nq\x00)\x81q\x01X\x03\x00\x00\x00fooq\x02Ns.'
>>> pickle.loads(pickle.dumps(ld))
{'foo': None}
>>> type(pickle.loads(pickle.dumps(ld)))
<class '__main__.LowerDict'>
__repr__
Чи потрібно мені перевидання, оновлення та __init__
?
Ми визначили update
і __init__
, але у вас є гарний __repr__
за замовчуванням:
>>> ld # without __repr__ defined for the class, we get this
{'foo': None}
Однак добре написати __repr__
атрибут для покращення налагоджуваності вашого коду. Ідеальний тест - це eval(repr(obj)) == obj
. Якщо це легко зробити для вашого коду, я настійно рекомендую:
>>> ld = LowerDict({})
>>> eval(repr(ld)) == ld
True
>>> ld = LowerDict(dict(a=1, b=2, c=3))
>>> eval(repr(ld)) == ld
True
Розумієте, саме те, що нам потрібно відтворити еквівалентний об’єкт - це те, що може відображатися в наших журналах або в ретракціях:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Висновок
Чи повинен я просто використовувати mutablemapping
(здається, один не повинен використовувати UserDict
або DictMixin
)? Якщо так, то як? Документи не зовсім освічуючі.
Так, це ще кілька рядків коду, але вони мають бути вичерпними. Першим моїм схильністю було б використовувати прийняту відповідь, і якщо з цим виникнуть проблеми, я б тоді подивився на свою відповідь - оскільки це трохи складніше, і немає АВС, яка допомогла б мені правильно встановити інтерфейс.
Передчасна оптимізація потребує більшої складності у пошуку ефективності.
MutableMapping
простіше - тому він отримує негайний край, а всі інші рівні. Тим не менш, щоб викласти всі відмінності, давайте порівняємо та порівняємо.
Варто додати, що був поштовх, щоб помістити аналогічний словник у collections
модуль, але він був відхилений . Вам, мабуть, слід просто зробити це замість цього:
my_dict[transform(key)]
Це має бути набагато легше для відсвідчення.
Порівнювати і протиставляти
Є 6 функцій інтерфейсу, реалізованих з MutableMapping
(який відсутній fromkeys
) та 11 з dict
підкласом. Мені не потрібно , щоб реалізувати __iter__
або __len__
, але замість цього я повинен реалізувати get
, setdefault
, pop
, update
, copy
, __contains__
, і fromkeys
- але це досить тривіально, так як я можу використовувати спадкування для більшості з цих реалізацій.
У MutableMapping
реалізують деякі речі в Python , який dict
реалізує в C - так що я очікував би , що dict
підклас більш продуктивний в деяких випадках.
Ми отримуємо безкоштовно __eq__
в обох підходах - обидва з них припускають рівність лише в тому випадку, якщо інший диктант є малі, - але, знову ж таки, я думаю, що dict
підклас порівняється швидше.
Підсумок:
- підкласифікація
MutableMapping
простіша з меншими можливостями для помилок, але повільніше, займає більше пам’яті (див. зайвий диктант) і не дає змогиisinstance(x, dict)
- підкласифікація
dict
швидша, використовує менше пам'яті та проходить isinstance(x, dict)
, але вона має більшу складність в реалізації.
Що досконаліше? Це залежить від вашого визначення ідеального.