Comment "parfaitement" remplacer un dict?
comment rendre aussi parfaite que possible une sous-classe de dict ? Le but final est d'avoir un simple dict dans lequel les touches sont minuscules.
il semblerait qu'il devrait y avoir un tout petit ensemble de primitives que je peux outrepasser pour faire ce travail, mais selon toutes mes recherches et tentatives, il semble que ce n'est pas le cas:
-
Si je remplacer
__getitem__
/__setitem__
, puisget
/set
ne fonctionne pas. Comment puis-je le faire fonctionner? Je n'ai pas besoin de les mettre en œuvre individuellement. -
est-ce que j'empêche le décapage de fonctionner, et dois-je mettre en œuvre
__setstate__
etc.? -
Dois-je besoin
repr
,update
et__init__
? -
est-ce que je devrais juste utiliser mutablemapping (il semble qu'on ne devrait pas utiliser
UserDict
ouDictMixin
)? Si oui, comment? Les docteurs ne sont pas vraiment instructifs.
voici ma première tentative, get()
ne fonctionne pas et sans doute il ya beaucoup d'autres problèmes mineurs:
class arbitrary_dict(dict):
"""A dictionary that applies an arbitrary key-altering function
before accessing the keys."""
def __keytransform__(self, key):
return key
# Overridden methods. List from
# https://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict
def __init__(self, *args, **kwargs):
self.update(*args, **kwargs)
# Note: I'm using dict directly, since super(dict, self) doesn't work.
# I'm not sure why, perhaps dict is not a new-style class.
def __getitem__(self, key):
return dict.__getitem__(self, self.__keytransform__(key))
def __setitem__(self, key, value):
return dict.__setitem__(self, self.__keytransform__(key), value)
def __delitem__(self, key):
return dict.__delitem__(self, self.__keytransform__(key))
def __contains__(self, key):
return dict.__contains__(self, self.__keytransform__(key))
class lcdict(arbitrary_dict):
def __keytransform__(self, key):
return str(key).lower()
5 réponses
vous pouvez écrire un objet qui se comporte comme un dict assez facilement avec ABC s (Abstract Base Classes) du module collections . Il vous indique même si vous avez manqué une méthode, voici donc la version minimale qui ferme L'ABC.
import collections
class TransformedDict(collections.MutableMapping):
"""A dictionary that applies an arbitrary key-altering
function before accessing the keys"""
def __init__(self, *args, **kwargs):
self.store = dict()
self.update(dict(*args, **kwargs)) # use the free update to set keys
def __getitem__(self, key):
return self.store[self.__keytransform__(key)]
def __setitem__(self, key, value):
self.store[self.__keytransform__(key)] = value
def __delitem__(self, key):
del self.store[self.__keytransform__(key)]
def __iter__(self):
return iter(self.store)
def __len__(self):
return len(self.store)
def __keytransform__(self, key):
return key
vous obtenez quelques méthodes gratuites de L'ABC:
class MyTransformedDict(TransformedDict):
def __keytransform__(self, key):
return key.lower()
s = MyTransformedDict([('Test', 'test')])
assert s.get('TEST') is s['test'] # free get
assert 'TeSt' in s # free __contains__
# free setdefault, __eq__, and so on
import pickle
assert pickle.loads(pickle.dumps(s)) == s
# works too since we just use a normal dict
Je ne classerais pas dict
(ou d'autres builtins) directement. Il ne fait souvent pas sens, parce que ce que vous voulez réellement faire est mettre en œuvre l'interface d'un dict . Et c'est exactement ce que Abc.
comment rendre aussi "parfaite" que possible une sous-classe de dict?
le but final est d'avoir un dict simple dans lequel les touches sont en minuscules.
Si je remplace
__getitem__
/__setitem__
, puis get/set ne fonctionnent pas. Comment dois-je le faire fonctionner? Sûrement, je n'ai pas besoin de les mettre en œuvre individuellement?est-ce que j'empêche le décapage de travailler, et dois-je mettre en œuvre
__setstate__
etc?ai-je besoin repr, mise à jour et
__init__
?devrais - je juste utiliser
mutablemapping
(il semble qu'on ne devrait pas utiliserUserDict
ouDictMixin
)? Si oui, comment? Les docteurs ne sont pas vraiment instructifs.
la réponse acceptée serait ma première approche, mais depuis il a quelques problèmes,
et puisque personne n'a abordé l'alternative, en fait sous-titrant un dict
, je vais le faire ici.
Qu'est-ce qui ne va pas avec la réponse acceptée?
cela ressemble à une requête assez simple pour moi:
comment rendre aussi "parfaite" que possible une sous-classe de dict? Le but final est d'avoir un simple dict dans lequel les touches sont en minuscules.
la réponse acceptée n'est pas en fait la sous-classe dict
, et un test pour cela échoue:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
idéalement, n'importe quel code de vérification de type serait un test pour l'interface que nous attendons, ou une classe de base abstraite, mais si nos objets de données sont passés dans des fonctions qui testent pour dict
-et nous ne pouvons pas "corriger" ces fonctions, ce code échouera.
autres chicanes qu'on pourrait faire:
- la réponse acceptée manque aussi la méthode de classe:
fromkeys
. -
la réponse acceptée a aussi un redondant
__dict__
- donc en prenant plus d'espace dans la mémoire:>>> s.foo = 'bar' >>> s.__dict__ {'foo': 'bar', 'store': {'test': 'test'}}
effectivement sousclassant 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 assure que les clés sont passées dans le dict en minuscules si ce sont des cordes.
si je remplace
__getitem__
/__setitem__
, alors get/set ne fonctionne pas. Comment je les fais marcher? Je n'ai pas besoin de les mettre en œuvre individuellement.
bien, les mettre en œuvre individuellement est l'inconvénient de cette approche et l'avantage d'utiliser MutableMapping
(voir la réponse acceptée), mais ce n'est vraiment pas beaucoup plus de travail.
First, faisons la différence entre Python 2 et 3, Créez un singleton ( _RaiseKeyError
) pour être sûr que nous savons si nous obtenons un argument à dict.pop
, et créez une fonction pour nous assurer que nos touches string 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 tous les arguments 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 employons une approche presque chaudière-Plaque pour n'importe quelle méthode ou méthode spéciale Cela fait référence à une clé, mais autrement, par héritage, nous obtenons des méthodes: len
, clear
, items
, keys
, popitem
, et values
gratuitement. Tout cela nécessite une réflexion approfondie pour obtenir le droit, il est trivial de voir que cela fonctionne.
(notez que haskey
a été déprécié en Python 2, supprimé en Python 3.)
voici quelques usages:
>>> 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 la sous-classe DCT pickles just fine:
>>> 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 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ébugibilité de votre code. Le test idéal est eval(repr(obj)) == obj
. Si c'est facile 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 logs ou dans les backraces:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Conclusion
devrais-je juste utiliser
mutablemapping
(il semble qu'on ne devrait pas utiliserUserDict
ouDictMixin
)? Si oui, comment? Les docteurs 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 ça, 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 droit.
optimisation prématurée va pour une plus grande complexité dans la recherche de la performance.
MutableMapping
est plus simple - il obtient donc un bord immédiat, tout le reste étant égal. Néanmoins, pour exposer toutes les différences, comparons et contrastons.
je devrais ajouter qu'il y avait une poussée pour mettre un dictionnaire semblable dans le collections
module, mais il a été rejeté . Tu devrais probablement faire ça à la place.:
my_dict[transform(key)]
il devrait être beaucoup plus facilement contestable.
comparer et contraster
il y a 6 fonctions d'interface implémentées avec la MutableMapping
(qui manque fromkeys
) et 11 avec la sous-classe dict
. Je n'ai pas besoin d'implémenter __iter__
ou __len__
, mais je dois implémenter get
, setdefault
, pop
, update
, copy
, __contains__
, et fromkeys
- mais ceux-ci sont assez triviaux, puisque je peux utiliser l'héritage pour la plupart de ces implémentations.
le MutableMapping
implémente certaines choses en Python que dict
implémente en C - donc je m'attendrais à ce qu'une sous-classe dict
soit plus performante dans certains cas.
Nous obtenir un gratuitement __eq__
dans les deux approches qui supposent l'égalité seulement si un autre dict est minuscule - mais encore une fois, je pense que le dict
la sous-classe comparera plus rapidement.
résumé:
- sousclassing
MutableMapping
est plus simple avec moins d'opportunités pour les bogues, mais plus lent, prend plus de mémoire( voir dict redondant), et échoueisinstance(x, dict)
- sous-classement
dict
est plus rapide, utilise moins de mémoire, et passeisinstance(x, dict)
, mais il a une plus grande complexité à mettre en œuvre.
qui est plus parfait? Cela dépend de votre définition de parfait.
mes exigences étaient un peu plus strictes:
- j'ai dû conserver les informations de cas (les chaînes sont des chemins vers les fichiers affichés à l'utilisateur, mais c'est une application windows donc en interne toutes les opérations doivent être insensibles à la casse)
- j'avais besoin de clés aussi petites que possible (il a fait faire une différence dans la performance de la mémoire, coupé 110 Mo sur 370). Cela signifie que la mise en cache de la version en minuscules des clés n'est pas une option.
- j'ai eu besoin de la création des structures de données pour être aussi rapide que possible (encore une fois fait une différence dans la performance, la vitesse cette fois). Je devais aller avec un builtin
ma pensée initiale était de remplacer notre classe de chemin clunky par une sous-classe unicode insensible à la casse-mais:
- s'est avéré difficile à obtenir ce droit-voir: une classe de chaîne de caractères insensible à la casse en python
- il s'avère que la manipulation explicite des clés dict rend le code verbeux et brouillon - et sujet aux erreurs (les structures sont passées de part et d'autre, et il n'est pas clair si elles ont des instances CIStr comme clés/éléments, facile à oublier plus
some_dict[CIstr(path)]
est laid)
donc j'ai dû finalement écrire cette affaire insensible dict. Grâce au code de @AaronHall qui a été rendu 10 fois plus facile.
class CIstr(unicode):
"""See https://stackoverflow.com/a/43122305/281545, especially for inlines"""
__slots__ = () # does make a difference in memory performance
#--Hash/Compare
def __hash__(self):
return hash(self.lower())
def __eq__(self, other):
if isinstance(other, CIstr):
return self.lower() == other.lower()
return NotImplemented
def __ne__(self, other):
if isinstance(other, CIstr):
return self.lower() != other.lower()
return NotImplemented
def __lt__(self, other):
if isinstance(other, CIstr):
return self.lower() < other.lower()
return NotImplemented
def __ge__(self, other):
if isinstance(other, CIstr):
return self.lower() >= other.lower()
return NotImplemented
def __gt__(self, other):
if isinstance(other, CIstr):
return self.lower() > other.lower()
return NotImplemented
def __le__(self, other):
if isinstance(other, CIstr):
return self.lower() <= other.lower()
return NotImplemented
#--repr
def __repr__(self):
return '{0}({1})'.format(type(self).__name__,
super(CIstr, self).__repr__())
def _ci_str(maybe_str):
"""dict keys can be any hashable object - only call CIstr if str"""
return CIstr(maybe_str) if isinstance(maybe_str, basestring) else maybe_str
class LowerDict(dict):
"""Dictionary that transforms its keys to CIstr instances.
Adapted from: https://stackoverflow.com/a/39375731/281545
"""
__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, 'iteritems'):
mapping = getattr(mapping, 'iteritems')()
return ((_ci_str(k), v) for k, v in
chain(mapping, getattr(kwargs, 'iteritems')()))
def __init__(self, mapping=(), **kwargs):
# dicts take a mapping or iterable as their optional first argument
super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
def __getitem__(self, k):
return super(LowerDict, self).__getitem__(_ci_str(k))
def __setitem__(self, k, v):
return super(LowerDict, self).__setitem__(_ci_str(k), v)
def __delitem__(self, k):
return super(LowerDict, self).__delitem__(_ci_str(k))
def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
return type(self)(self)
def get(self, k, default=None):
return super(LowerDict, self).get(_ci_str(k), default)
def setdefault(self, k, default=None):
return super(LowerDict, self).setdefault(_ci_str(k), default)
__no_default = object()
def pop(self, k, v=__no_default):
if v is LowerDict.__no_default:
# super will raise KeyError if no default and key does not exist
return super(LowerDict, self).pop(_ci_str(k))
return super(LowerDict, self).pop(_ci_str(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__(_ci_str(k))
@classmethod
def fromkeys(cls, keys, v=None):
return super(LowerDict, cls).fromkeys((_ci_str(k) for k in keys), v)
def __repr__(self):
return '{0}({1})'.format(type(self).__name__,
super(LowerDict, self).__repr__())
implicite vs explicite est toujours un problème, mais une fois la poussière retombée, renommer les attributs/variables pour commencer avec ci (et un commentaire de big fat doc expliquant que ci signifie cas insensible) je pense est une solution parfaite - comme les lecteurs du code doit être pleinement conscient que nous avons affaire à Cas insensible structures de données sous-jacentes. J'espère que cela va corriger quelques bugs difficiles à reproduire, que je soupçonne de se réduire à la sensibilité de cas.
Commentaires/corrections sont les bienvenus :)
Tout ce que vous avez à faire est de
class BatchCollection(dict):
def __init__(self, *args, **kwargs):
dict.__init__(*args, **kwargs)
ou
class BatchCollection(dict):
def __init__(self, inpt={}):
super(BatchCollection, self).__init__(inpt)
Un exemple d'utilisation pour mon usage personnel
### EXAMPLE
class BatchCollection(dict):
def __init__(self, inpt={}):
dict.__init__(*args, **kwargs)
def __setitem__(self, key, item):
if (isinstance(key, tuple) and len(key) == 2
and isinstance(item, collections.Iterable)):
# self.__dict__[key] = item
super(BatchCollection, self).__setitem__(key, item)
else:
raise Exception(
"Valid key should be a tuple (database_name, table_name) "
"and value should be iterable")
Note : testé uniquement en python3
après avoir testé les deux suggestions du top 1519190920" deux , j'ai choisi une voie médiane ombragée pour Python 2.7. Peut-être 3 est plus sain, mais pour moi:
class MyDict(MutableMapping):
# ... the few __methods__ that mutablemapping requires
# and then this monstrosity
@classmethod
def __class__(cls):
return dict
que je déteste vraiment, mais qui semble correspondre à mes besoins, qui sont:
- peut remplacer
**my_dict
- si vous héritez de
dict
, ceci contourne votre code . de l'essayer. - Cela fait #2 inacceptable pour moi en tout temps , car c'est assez courant dans le code python
- si vous héritez de
- mascarades
isinstance(my_dict, dict)
- comportement entièrement contrôlable
- donc je ne peux pas hériter de
dict
- donc je ne peux pas hériter de
si vous avez besoin de vous distinguer des autres, personnellement j'utilise quelque chose comme ceci (bien que je recommande de meilleurs noms):
def __am_i_me(self):
return True
@classmethod
def __is_it_me(cls, other):
try:
return other.__am_i_me()
except Exception:
return False
tant que vous n'avez besoin de vous reconnaître qu'en interne, c'est plus difficile pour appeler accidentellement __am_i_me
en raison du nom de Python-munging (ceci est renommé en _MyDict__am_i_me
de tout ce qui appelle en dehors de cette classe). Un peu plus privé que _method
s, à la fois dans la pratique et culturellement.
jusqu'à présent, je n'ai pas de plaintes, à part le __class__
qui a l'air très louche. Je serais ravi d'entendre parler de tous les problèmes que d'autres rencontrent avec ce bien, je ne comprends pas pleinement les conséquences. Mais jusqu'à présent, je n'ai eu aucun problème, et cela m'a permis de migrer beaucoup de code de qualité moyenne dans beaucoup d'endroits sans avoir besoin de changements.
comme preuve: https://repl.it/repls/TraumaticToughCockatoo
essentiellement: copier l'option actuelle #2 , ajouter print 'method_name'
lignes à chaque méthode, puis essayer ceci et regarder la sortie:
d = LowerDict() # prints "init", or whatever your print statement said
print '------'
splatted = dict(**d) # note that there are no prints here
vous verrez un comportement similaire pour d'autres scénarios. Dites que votre faux- dict
est un wrapper autour d'un autre type de données, donc il n'y a pas de façon raisonnable de stocker les données dans le dos-dict; **your_dict
sera vide, indépendamment de ce que chaque autre méthode fait.
Cela fonctionne correctement pour MutableMapping
, mais dès que vous héritez de dict
il devient incontrôlable.