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 dicte 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.pope 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 supercon 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 valuesgratuitamente. Anche se questo ha richiesto un'attenta riflessione per avere ragione, è banale vedere che funziona.
(Nota che haskeyera 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 updatee __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 collectionsmodulo, 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 dictsottoclasse. 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' MutableMappingimplementazione di alcune cose in Python che dictimplementa in C, quindi mi aspetto che una dictsottoclasse 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 dictsottoclasse 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.