Comment créer une sous-classe de dict aussi «parfaite» que possible?
L'objectif final est d'avoir un dict simple dans lequel les touches sont en minuscules.
Si je remplace __getitem__
/ __setitem__
, alors get / set ne fonctionne pas. Comment les faire fonctionner? Je n'ai sûrement pas besoin de les mettre en œuvre individuellement?
Suis-je empêcher le décapage de fonctionner, et dois-je mettre en œuvre
__setstate__
etc?
Ai-je besoin de repr, mise à jour et __init__
?
Dois-je simplement utiliser mutablemapping
(il semble que l'on ne devrait pas utiliser UserDict
ou DictMixin
)? Si c'est le cas, comment? Les documents ne sont pas vraiment instructifs.
La réponse acceptée serait ma première approche, mais comme il y a des problèmes et que personne n'a abordé l'alternative, en fait la sous-classe a dict
, je vais le faire ici.
Quel est le problème avec la réponse acceptée?
Cela me semble être une demande assez simple:
Comment créer une sous-classe de dict aussi «parfaite» que possible? L'objectif final est d'avoir un dict simple dans lequel les touches sont en minuscules.
La réponse acceptée ne fait pas réellement partie de la sous dict
- classe , et un test pour cela échoue:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
Idéalement, tout code de vérification de type testerait l'interface que nous attendons, ou une classe de base abstraite, mais si nos objets de données sont passés à des fonctions qui testent dict
- et nous ne pouvons pas "corriger" ces fonctions, ce code échouera.
D'autres problèmes que l'on pourrait faire:
- La réponse acceptée manque également la méthode de classe:
fromkeys
.
La réponse acceptée a également une redondance __dict__
- prenant donc plus de place en mémoire:
>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
Sous-classement réel dict
Nous pouvons réutiliser les méthodes dict par héritage. Tout ce que nous devons faire est de créer une couche d'interface qui garantit que les clés sont passées dans le dict en minuscules s'il s'agit de chaînes.
Si je remplace __getitem__
/ __setitem__
, alors get / set ne fonctionne pas. Comment les faire fonctionner? Je n'ai sûrement pas besoin de les mettre en œuvre individuellement?
Eh bien, les mettre en œuvre individuellement est l'inconvénient de cette approche et l'avantage de l'utiliser MutableMapping
(voir la réponse acceptée), mais ce n'est vraiment pas beaucoup plus de travail.
Tout d'abord, factorisons la différence entre Python 2 et 3, créons un singleton ( _RaiseKeyError
) pour nous assurer que nous savons si nous obtenons réellement un argument dict.pop
et créons une fonction pour nous assurer que nos clés de chaîne sont en minuscules:
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
Maintenant, nous implémentons - j'utilise super
avec les arguments complets pour que ce code fonctionne pour Python 2 et 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__())
Nous utilisons une approche presque plaque de chaudière pour toute méthode ou méthode spéciale qui fait référence à une clé, mais autrement, par héritage, nous obtenons des méthodes: len
, clear
, items
, keys
, popitem
et values
gratuitement. Bien que cela ait nécessité une réflexion approfondie pour bien faire les choses, il est trivial de voir que cela fonctionne.
(Notez que cet haskey
élément a été déconseillé dans Python 2, supprimé dans Python 3.)
Voici quelques utilisations:
>>> 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)
Suis-je empêcher le décapage de fonctionner, et dois-je mettre en œuvre
__setstate__
etc?
décapage
Et les cornichons de sous-classe dict très bien:
>>> 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__
Ai-je besoin de repr, mise à jour et __init__
?
Nous avons défini update
et __init__
, mais vous avez une belle __repr__
par défaut:
>>> ld # without __repr__ defined for the class, we get this
{'foo': None}
Cependant, il est bon d'écrire un __repr__
pour améliorer la débogabilité de votre code. Le test idéal est eval(repr(obj)) == obj
. Si c'est facile à faire pour votre code, je le recommande fortement:
>>> ld = LowerDict({})
>>> eval(repr(ld)) == ld
True
>>> ld = LowerDict(dict(a=1, b=2, c=3))
>>> eval(repr(ld)) == ld
True
Vous voyez, c'est exactement ce dont nous avons besoin pour recréer un objet équivalent - c'est quelque chose qui pourrait apparaître dans nos journaux ou dans les backtraces:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Conclusion
Dois-je simplement utiliser mutablemapping
(il semble que l'on ne devrait pas utiliser UserDict
ou DictMixin
)? Si c'est le cas, comment? Les documents ne sont pas vraiment instructifs.
Oui, ce sont quelques lignes de code supplémentaires, mais elles sont destinées à être complètes. Ma première inclination serait d'utiliser la réponse acceptée, et s'il y avait des problèmes avec elle, je regarderais alors ma réponse - car c'est un peu plus compliqué, et il n'y a pas d'ABC pour m'aider à obtenir mon interface correcte.
L'optimisation prématurée va vers une plus grande complexité à la recherche de performances.
MutableMapping
est plus simple - il obtient donc un avantage immédiat, toutes choses étant égales par ailleurs. Néanmoins, pour exposer toutes les différences, comparons et contrastons.
Je dois ajouter qu'il y a eu une pression pour mettre un dictionnaire similaire dans le collections
module, mais il a été rejeté . Vous devriez probablement le faire à la place:
my_dict[transform(key)]
Il devrait être beaucoup plus facile à déboguer.
Compare et nuance
Il y a 6 fonctions d'interface implémentées avec MutableMapping
(ce qui manque fromkeys
) et 11 avec la dict
sous - classe. Je ne ai pas besoin de mettre en œuvre __iter__
ou __len__
, mais je dois mettre en œuvre get
, setdefault
, pop
, update
, copy
, __contains__
etfromkeys
- mais ceux - ci sont assez trivial, puisque je peux utiliser l' héritage pour la plupart de ces implémentations.
L' MutableMapping
implémente certaines choses en Python qui dict
implémentent en C - donc je m'attends à ce qu'une dict
sous - classe soit plus performante dans certains cas.
Nous obtenons une gratuité __eq__
dans les deux approches - qui n'assument l'égalité que si un autre dict est tout en minuscules - mais encore une fois, je pense que la dict
sous - classe se comparera plus rapidement.
Résumé:
- le sous
MutableMapping
- classement est plus simple avec moins de possibilités de bogues, mais plus lent, prend plus de mémoire (voir dict redondant) et échoueisinstance(x, dict)
- le sous
dict
- classement est plus rapide, utilise moins de mémoire et passe isinstance(x, dict)
, mais sa mise en œuvre est plus complexe.
Quel est le plus parfait? Cela dépend de votre définition de parfait.