Jak mogę uczynić „perfekcyjną” podklasę dyktku, jak to możliwe?
Ostatecznym celem jest prosty dyktando, w którym klawisze są pisane małymi literami.
Jeśli zastąpię __getitem__
/ __setitem__
, wtedy get / set nie działa. Jak sprawić, by działały? Z pewnością nie muszę wdrażać ich indywidualnie?
Czy zapobiegam działaniu wytrawiania i czy muszę wdrażać
__setstate__
itp.?
Czy potrzebuję powtórzenia, aktualizacji i __init__
?
Czy powinienem użyć mutablemapping
(wydaje się, że nie należy używać UserDict
lub DictMixin
)? Jeśli tak to jak? Dokumenty nie są do końca pouczające.
Przyjęta odpowiedź byłaby moim pierwszym podejściem, ale ponieważ ma pewne problemy, a ponieważ nikt nie zajął się alternatywą, właściwie podklasą a dict
, zrobię to tutaj.
Co jest nie tak z zaakceptowaną odpowiedzią?
Wydaje mi się to dość prostą prośbą:
Jak mogę uczynić „perfekcyjną” podklasę dyktku, jak to możliwe? Ostatecznym celem jest prosty dyktando, w którym klawisze są pisane małymi literami.
Przyjęta odpowiedź nie jest właściwie podklasą dict
, a test na to się nie powiedzie:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
Idealnie byłoby, gdyby każdy kod sprawdzający typ testowałby oczekiwany interfejs lub abstrakcyjną klasę podstawową, ale jeśli nasze obiekty danych są przekazywane do funkcji, które testują dict
- i nie możemy „naprawić” tych funkcji, ten kod zawiedzie.
Inne sprzeczki, które można zrobić:
- Odpowiedź Akceptowane jest również brakuje classmethod:
fromkeys
.
Przyjęta odpowiedź ma również nadmiar __dict__
- dlatego zajmuje więcej miejsca w pamięci:
>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
Właściwie podklasę dict
Możemy ponownie wykorzystać metody dict poprzez dziedziczenie. Wszystko, co musimy zrobić, to stworzyć warstwę interfejsu, która zapewni, że klucze będą przekazywane do nagrania małymi literami, jeśli są łańcuchami.
Jeśli zastąpię __getitem__
/ __setitem__
, wtedy get / set nie działa. Jak sprawić, by działały? Z pewnością nie muszę wdrażać ich indywidualnie?
Cóż, wdrażanie ich osobno jest wadą tego podejścia i zaletą używania MutableMapping
(patrz zaakceptowana odpowiedź), ale tak naprawdę nie jest to dużo więcej pracy.
Po pierwsze, rozłóżmy różnicę między Pythonem 2 i 3, utwórz singleton ( _RaiseKeyError
), aby upewnić się, że rzeczywiście otrzymujemy argument dict.pop
, i utwórz funkcję, aby nasze klucze łańcuchowe były pisane małymi literami:
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
Teraz implementujemy - używam super
z pełnymi argumentami, aby ten kod działał dla Pythona 2 i 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__())
Używamy podejście niemal bojler-płyta dla dowolnej metody lub specjalną metodą, która odwołuje się kluczowym, ale w inny sposób, przez dziedziczenie, otrzymujemy metody: len
, clear
, items
, keys
, popitem
, i values
za darmo. Choć wymagało to starannego przemyślenia, nie jest łatwo dostrzec, że to działa.
(Uwaga: haskey
przestarzała w Pythonie 2, usunięta w Pythonie 3)
Oto niektóre zastosowania:
>>> 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)
Czy zapobiegam działaniu wytrawiania i czy muszę wdrażać
__setstate__
itp.?
marynowanie
A pikle podklasy dict są w porządku:
>>> 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__
Czy potrzebuję powtórzenia, aktualizacji i __init__
?
Zdefiniowaliśmy update
i __init__
, ale __repr__
domyślnie masz piękny :
>>> ld # without __repr__ defined for the class, we get this
{'foo': None}
__repr__
Warto jednak napisać a, aby poprawić debugowanie kodu. Idealny test to eval(repr(obj)) == obj
. Jeśli jest to łatwe do zrobienia dla twojego kodu, zdecydowanie go polecam:
>>> ld = LowerDict({})
>>> eval(repr(ld)) == ld
True
>>> ld = LowerDict(dict(a=1, b=2, c=3))
>>> eval(repr(ld)) == ld
True
Widzisz, dokładnie tego potrzebujemy do odtworzenia równoważnego obiektu - jest to coś, co może pojawić się w naszych dziennikach lub śladach:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Wniosek
Czy powinienem użyć mutablemapping
(wydaje się, że nie należy używać UserDict
lub DictMixin
)? Jeśli tak to jak? Dokumenty nie są do końca pouczające.
Tak, to jeszcze kilka wierszy kodu, ale mają one być wyczerpujące. Moją pierwszą skłonnością byłoby użyć zaakceptowanej odpowiedzi, a jeśli byłyby z nią jakieś problemy, spojrzałbym na moją odpowiedź - ponieważ jest to trochę bardziej skomplikowane i nie ma ABC, który pomógłby mi poprawnie ustawić interfejs.
Przedwczesna optymalizacja dąży do większej złożoności w poszukiwaniu wydajności.
MutableMapping
jest prostszy - dzięki czemu uzyskuje natychmiastową przewagę, a wszystkie pozostałe są równe. Niemniej jednak, aby przedstawić wszystkie różnice, porównajmy i skontrastujmy.
Powinienem dodać, że pojawił się nacisk na umieszczenie podobnego słownika w collections
module, ale został on odrzucony . Prawdopodobnie powinieneś po prostu zrobić to zamiast tego:
my_dict[transform(key)]
Powinno być znacznie łatwiejsze do debugowania.
Porównać i kontrastować
Zaimplementowano 6 funkcji interfejsu z MutableMapping
(której brakuje fromkeys
) i 11 z dict
podklasą. Nie muszę wdrożyć __iter__
lub __len__
, lecz muszę wdrożyć get
, setdefault
, pop
, update
, copy
, __contains__
, i fromkeys
- ale są to dość trywialne, ponieważ mogę używać dziedziczenia dla większości z tych wdrożeń.
Te MutableMapping
narzędzia pewne rzeczy w Pythonie, które dict
wdraża w C - więc spodziewałbym się dict
podklasa być bardziej wydajnych w niektórych przypadkach.
Dostajemy darmowe __eq__
w obu podejściach - oba zakładają równość tylko wtedy, gdy inny dykt jest pisany małymi literami - ale znowu, myślę, że dict
podklasa porówna się szybciej.
Podsumowanie:
- podklasowanie
MutableMapping
jest prostsze z mniejszą liczbą błędów, ale wolniejsze, zajmuje więcej pamięci (patrz redundantny dykt) i kończy się niepowodzeniemisinstance(x, dict)
- podklasowanie
dict
jest szybsze, zużywa mniej pamięci i przechodzi isinstance(x, dict)
, ale ma większą złożoność do wdrożenia.
Który jest bardziej idealny? To zależy od twojej definicji ideału.