Come posso rendere il più "perfetto" possibile una sottoclasse di dict?
L'obiettivo finale è avere un semplice dict in cui i tasti sono minuscoli.
Se sovrascrivo __getitem__
/ __setitem__
, allora get / set non funziona. Come li faccio funzionare? Sicuramente non ho bisogno di implementarli individualmente?
Sto impedendo il funzionamento del decapaggio e devo implementare
__setstate__
ecc.?
Ho bisogno di ristampa, aggiornamento e __init__
?
Dovrei solo usare mutablemapping
(sembra che uno non dovrebbe usare UserDict
o DictMixin
)? Se é cosi, come? I documenti non sono esattamente illuminanti.
La risposta accettata sarebbe il mio primo approccio, ma poiché ha alcuni problemi e poiché nessuno ha affrontato l'alternativa, in realtà sottoclasse a dict
, lo farò qui.
Cosa c'è di sbagliato nella risposta accettata?
Questa mi sembra una richiesta piuttosto semplice:
Come posso rendere il più "perfetto" possibile una sottoclasse di dict? L'obiettivo finale è avere un semplice dict in cui i tasti sono minuscoli.
La risposta accettata in realtà non è una sottoclasse dict
e un test per questo fallisce:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
Idealmente, qualsiasi codice di controllo del tipo verrebbe testato per l'interfaccia che ci aspettiamo o una classe base astratta, ma se i nostri oggetti dati vengono passati a funzioni che stanno testando dict
- e non possiamo "aggiustare" quelle funzioni, questo codice avrà esito negativo.
Altri cavilli che si potrebbero fare:
- La risposta accettata manca anche il classmethod:
fromkeys
.
La risposta accettata ha anche una ridondanza __dict__
, quindi occupa più spazio in memoria:
>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
In realtà sottoclasse dict
Possiamo riutilizzare i metodi dict attraverso l'ereditarietà. Tutto quello che dobbiamo fare è creare un livello di interfaccia che assicuri che le chiavi vengano passate nel dict in forma minuscola se sono stringhe.
Se sovrascrivo __getitem__
/ __setitem__
, allora get / set non funziona. Come li faccio funzionare? Sicuramente non ho bisogno di implementarli individualmente?
Bene, implementarli singolarmente è lo svantaggio di questo approccio e il lato positivo dell'utilizzo MutableMapping
(vedi la risposta accettata), ma in realtà non è molto più lavoro.
Innanzitutto, escludiamo la differenza tra Python 2 e 3, creiamo un singleton ( _RaiseKeyError
) per assicurarci di sapere se otteniamo effettivamente un argomento dict.pop
e creiamo una funzione per assicurarci che le nostre chiavi di stringa siano minuscole:
from itertools import chain
try:
str_base = basestring
items = 'iteritems'
except NameError:
str_base = str, bytes, bytearray
items = 'items'
_RaiseKeyError = object()
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
Ora implementiamo: sto usando super
con gli argomenti completi in modo che questo codice funzioni per Python 2 e 3:
class LowerDict(dict):
__slots__ = ()
@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):
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__())
Usiamo un approccio quasi caldaia-piastra per qualsiasi metodo o metodo speciale che i riferimenti di una chiave, ma per il resto, per eredità, otteniamo metodi: len
, clear
, items
, keys
, popitem
, e values
gratuitamente. Anche se questo ha richiesto un'attenta riflessione per avere ragione, è banale vedere che funziona.
(Nota che haskey
era deprecato in Python 2, rimosso in Python 3.)
Ecco alcuni utilizzi:
>>> 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)
Sto impedendo il funzionamento del decapaggio e devo implementare
__setstate__
ecc.?
decapaggio
E la sottoclasse dict va benissimo:
>>> 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__
Ho bisogno di ristampa, aggiornamento e __init__
?
Abbiamo definito update
e __init__
, ma hai una bella __repr__
di default:
>>> ld
{'foo': None}
Tuttavia, è bene scrivere a __repr__
per migliorare il debug del codice. Il test ideale è eval(repr(obj)) == obj
. Se è facile da fare per il tuo codice, lo consiglio vivamente:
>>> ld = LowerDict({})
>>> eval(repr(ld)) == ld
True
>>> ld = LowerDict(dict(a=1, b=2, c=3))
>>> eval(repr(ld)) == ld
True
Vedi, è esattamente ciò di cui abbiamo bisogno per ricreare un oggetto equivalente - questo è qualcosa che potrebbe apparire nei nostri log o nei backtrace:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Conclusione
Dovrei solo usare mutablemapping
(sembra che uno non dovrebbe usare UserDict
o DictMixin
)? Se é cosi, come? I documenti non sono esattamente illuminanti.
Sì, queste sono alcune righe di codice in più, ma intendono essere complete. La mia prima inclinazione sarebbe quella di utilizzare la risposta accettata, e se ci fossero problemi con essa, allora guarderei la mia risposta - poiché è un po 'più complicata e non c'è ABC che mi aiuti a ottenere la mia interfaccia corretta.
L'ottimizzazione prematura sta andando per una maggiore complessità alla ricerca delle prestazioni.
MutableMapping
è più semplice, quindi ottiene un vantaggio immediato, a parità di tutto il resto. Tuttavia, per mettere in evidenza tutte le differenze, confrontiamo e confrontiamo.
Dovrei aggiungere che c'è stata una spinta per inserire un dizionario simile nel collections
modulo, ma è stato rifiutato . Probabilmente dovresti farlo invece:
my_dict[transform(key)]
Dovrebbe essere molto più facilmente eseguibile il debug.
Confrontare e contrapporre
Ci sono 6 funzioni di interfaccia implementate con MutableMapping
(che manca fromkeys
) e 11 con la dict
sottoclasse. Non ho bisogno di implementare __iter__
o __len__
, ma invece devo implementare get
, setdefault
, pop
, update
, copy
, __contains__
, e fromkeys
- ma questi sono abbastanza banale, dato che posso utilizzare l'ereditarietà per la maggior parte di queste implementazioni.
L' MutableMapping
implementazione di alcune cose in Python che dict
implementa in C, quindi mi aspetto che una dict
sottoclasse sia più performante in alcuni casi.
Otteniamo una libertà __eq__
in entrambi gli approcci - entrambi assumono l'uguaglianza solo se un altro dict è tutto minuscolo - ma ancora una volta, penso che la dict
sottoclasse si confronterà più rapidamente.
Sommario:
- la sottoclasse
MutableMapping
è più semplice con meno possibilità di bug, ma più lenta, richiede più memoria (vedi dict ridondante) e fallisceisinstance(x, dict)
- la sottoclasse
dict
è più veloce, utilizza meno memoria e passa isinstance(x, dict)
, ma ha una maggiore complessità da implementare.
Quale è più perfetto? Dipende dalla tua definizione di perfetto.