Wie kann ich eine Unterklasse von Diktaten so "perfekt" wie möglich machen?
Das Endziel ist ein einfaches Diktat, bei dem die Tasten in Kleinbuchstaben geschrieben sind.
Wenn ich __getitem__
/ überschreibe __setitem__
, funktioniert get / set nicht. Wie bringe ich sie zum Arbeiten? Sicherlich muss ich sie nicht einzeln implementieren?
Verhindere ich, dass das Beizen funktioniert, und muss ich __setstate__
usw. implementieren
?
Benötige ich Repr, Update und __init__
?
Soll ich nur verwenden mutablemapping
(es scheint, man sollte nicht verwenden UserDict
oder DictMixin
)? Wenn das so ist, wie? Die Dokumente sind nicht gerade aufschlussreich.
Die akzeptierte Antwort wäre mein erster Ansatz, aber da es einige Probleme gibt und niemand die Alternative angesprochen hat, die tatsächlich eine Unterklasse ist dict
, werde ich das hier tun.
Was ist falsch an der akzeptierten Antwort?
Dies scheint mir eine ziemlich einfache Bitte zu sein:
Wie kann ich eine Unterklasse von Diktaten so "perfekt" wie möglich machen? Das Endziel ist ein einfaches Diktat, bei dem die Tasten in Kleinbuchstaben geschrieben sind.
Die akzeptierte Antwort ist eigentlich keine Unterklasse dict
, und ein Test dafür schlägt fehl:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
Im Idealfall testet jeder Code zur Typprüfung die von uns erwartete Schnittstelle oder eine abstrakte Basisklasse. Wenn unsere Datenobjekte jedoch an Funktionen übergeben werden, auf die getestet wird dict
- und wir diese Funktionen nicht "reparieren" können, diesen Code wird versagen.
Andere Probleme, die man machen könnte:
- In der akzeptierten Antwort fehlt auch die Klassenmethode :
fromkeys
.
Die akzeptierte Antwort ist ebenfalls redundant __dict__
und nimmt daher mehr Speicherplatz ein:
>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
Eigentlich Unterklasse dict
Wir können die Diktiermethoden durch Vererbung wiederverwenden. Wir müssen lediglich eine Schnittstellenebene erstellen, die sicherstellt, dass Schlüssel in Kleinbuchstaben an das Diktat übergeben werden, wenn es sich um Zeichenfolgen handelt.
Wenn ich __getitem__
/ überschreibe __setitem__
, funktioniert get / set nicht. Wie bringe ich sie zum Arbeiten? Sicherlich muss ich sie nicht einzeln implementieren?
Die individuelle Implementierung ist der Nachteil dieses Ansatzes und der Vorteil der Verwendung MutableMapping
(siehe die akzeptierte Antwort), aber es ist wirklich nicht viel mehr Arbeit.
Lassen Sie uns zunächst den Unterschied zwischen Python 2 und 3 herausrechnen, ein Singleton ( _RaiseKeyError
) erstellen , um sicherzustellen, dass wir wissen, ob wir tatsächlich ein Argument erhalten dict.pop
, und eine Funktion erstellen, um sicherzustellen, dass unsere Zeichenfolgenschlüssel in Kleinbuchstaben geschrieben sind:
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
Jetzt implementieren wir - ich verwende super
mit den vollständigen Argumenten, damit dieser Code für Python 2 und 3 funktioniert:
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__())
Wir verwenden einen fast Kesselblech Ansatz für jedes Verfahren oder spezielle Methode , die Referenzen eines Schlüssel, aber ansonsten durch Vererbung erhalten wir Methoden: len
, clear
, items
, keys
, popitem
, und values
kostenlos. Dies erforderte einige sorgfältige Überlegungen, um richtig zu sein, aber es ist trivial zu sehen, dass dies funktioniert.
(Beachten Sie, dass dies haskey
in Python 2 veraltet und in Python 3 entfernt wurde.)
Hier ist eine Verwendung:
>>> 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)
Verhindere ich, dass das Beizen funktioniert, und muss ich __setstate__
usw. implementieren
?
Beizen
Und die dikt Unterklasse Gurken ganz gut:
>>> 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__
Benötige ich Repr, Update und __init__
?
Wir haben update
und definiert __init__
, aber Sie haben __repr__
standardmäßig eine schöne :
>>> ld # without __repr__ defined for the class, we get this
{'foo': None}
Es ist jedoch gut, eine zu schreiben __repr__
, um die Debugbarkeit Ihres Codes zu verbessern. Der ideale Test ist eval(repr(obj)) == obj
. Wenn es für Ihren Code einfach ist, empfehle ich es dringend:
>>> ld = LowerDict({})
>>> eval(repr(ld)) == ld
True
>>> ld = LowerDict(dict(a=1, b=2, c=3))
>>> eval(repr(ld)) == ld
True
Sie sehen, es ist genau das, was wir brauchen, um ein äquivalentes Objekt neu zu erstellen - dies kann in unseren Protokollen oder in Rückverfolgungen angezeigt werden:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Fazit
Soll ich nur verwenden mutablemapping
(es scheint, man sollte nicht verwenden UserDict
oder DictMixin
)? Wenn das so ist, wie? Die Dokumente sind nicht gerade aufschlussreich.
Ja, dies sind noch ein paar Codezeilen, aber sie sollen umfassend sein. Meine erste Neigung wäre, die akzeptierte Antwort zu verwenden, und wenn es Probleme damit gäbe, würde ich mir meine Antwort ansehen - da sie etwas komplizierter ist und es kein ABC gibt, das mir hilft, meine Benutzeroberfläche richtig zu machen.
Eine vorzeitige Optimierung führt zu einer höheren Komplexität bei der Suche nach Leistung.
MutableMapping
ist einfacher - so bekommt es einen sofortigen Vorteil, alles andere ist gleich. Um jedoch alle Unterschiede aufzuzeigen, vergleichen und kontrastieren wir sie.
Ich sollte hinzufügen, dass es einen Druck gab, ein ähnliches Wörterbuch in das collections
Modul aufzunehmen, aber es wurde abgelehnt . Sie sollten dies wahrscheinlich stattdessen einfach tun:
my_dict[transform(key)]
Es sollte viel einfacher zu debuggen sein.
Vergleichen und gegenüberstellen
Es gibt 6 Schnittstellenfunktionen, die mit der MutableMapping
(was fehlt fromkeys
) und 11 mit der dict
Unterklasse implementiert sind . Ich weiß nicht implementieren müssen __iter__
oder __len__
, sondern ich habe zu implementieren get
, setdefault
, pop
, update
, copy
, __contains__
, und fromkeys
- aber diese sind ziemlich trivial, da ich die Vererbung für die meisten dieser Implementierungen verwenden können.
Das MutableMapping
implementiert einige Dinge in Python, dict
die in C implementiert werden - daher würde ich erwarten, dass eine dict
Unterklasse in einigen Fällen leistungsfähiger ist.
Wir erhalten __eq__
in beiden Ansätzen ein freies - beide gehen nur dann von Gleichheit aus, wenn ein anderes Diktat nur in Kleinbuchstaben geschrieben ist -, aber ich denke, die dict
Unterklasse wird sich schneller vergleichen lassen.
Zusammenfassung:
- Unterklassen
MutableMapping
sind einfacher mit weniger Möglichkeiten für Fehler, aber langsamer, benötigen mehr Speicher (siehe redundantes Diktat) und schlagen fehlisinstance(x, dict)
- Unterklassen
dict
sind schneller, verbrauchen weniger Speicher und bestehen isinstance(x, dict)
, sind jedoch komplexer zu implementieren.
Welches ist perfekter? Das hängt von Ihrer Definition von perfekt ab.