Comment puis-je rendre aussi "parfaite" une sous-classe de dict aussi possible? L'objectif final est d'avoir un simple dict dans lequel les clés sont en minuscule.
Il semblerait qu'il devrait exister un petit ensemble de primitives que je puisse remplacer pour que cela fonctionne, mais selon toutes mes recherches et tentatives, il semble que ce ne soit pas le cas:
Si je remplacez __getitem__
/__setitem__
, alors get
/set
ne fonctionne pas. Comment puis-je les faire fonctionner? Je n'ai sûrement pas besoin de les mettre en œuvre individuellement?
Est-ce que j'empêche le marinage de fonctionner et dois-je implémenter __setstate__
Etc.?
Est-ce que je besoin de repr
, update
et de __init__
?
Devrais-je simplement tiliser mutablemapping (il semble qu'on ne devrait pas utiliser UserDict
ou DictMixin
)? Si c'est le cas, comment? Les documents ne sont pas vraiment éclairants.
Voici mon premier essai: get()
ne fonctionne pas et il y a sans doute 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()
Vous pouvez écrire un objet qui se comporte comme un dict assez facilement avec ABC s (classes de base abstraites) du module collections . Il vous indique même si vous avez manqué une méthode. La version minimale qui ferme l’ABC est indiquée ci-dessous.
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 sous-classerais pas dict
(ou d'autres éléments intégrés) directement. Cela n'a souvent aucun sens, car ce que vous voulez réellement faire est de implémenter l'interface d'un dict . Et c'est exactement ce que ABC est pour.
Comment puis-je rendre aussi "parfait" une sous-classe de dict autant que possible?
Le but final est d’avoir un dict simple dans lequel les clés sont en minuscule.
Si je remplace
__getitem__
/__setitem__
, Alors obtenir/définir ne fonctionne pas. Comment puis-je les faire fonctionner? Je n'ai sûrement pas besoin de les mettre en œuvre individuellement?Est-ce que j'empêche le marinage de fonctionner et dois-je implémenter
__setstate__
Etc.?Est-ce que j'ai besoin de repr, update et
__init__
?Devrais-je simplement utiliser
mutablemapping
(il semble qu'on ne devrait pas utiliserUserDict
ouDictMixin
)? Si c'est le cas, comment? Les documents ne sont pas vraiment éclairants.
La réponse acceptée serait ma première approche, mais comme il y a quelques problèmes et que personne n’a abordé l’alternative, en sous-classant un dict
, je vais le faire ici.
Cela me semble une requête assez simple:
Comment puis-je rendre aussi "parfait" une sous-classe de dict autant que possible? Le but final est d’avoir un dict simple dans lequel les clés sont en minuscule.
La réponse acceptée ne contient pas réellement la sous-classe dict
, et un test pour cela échoue:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
Idéalement, tout code de vérification de type teste 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 dict
- et nous ne pouvons pas "réparer" "ces fonctions, ce code va échouer.
On pourrait faire d'autres petits problèmes:
fromkeys
.La réponse acceptée a aussi un redondant __dict__
- occupant donc plus d'espace mémoire:
>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
dict
Nous pouvons réutiliser les méthodes dict par héritage. Tout ce que nous avons à faire est de créer une couche d’interface garantissant que les clés sont passées dans le dictionnaire en minuscule s’il s’agit de chaînes.
Si je remplace
__getitem__
/__setitem__
, Alors obtenir/définir ne fonctionne pas. Comment puis-je les faire fonctionner? Je n'ai sûrement pas besoin de les mettre en œuvre individuellement?
Eh bien, les implémenter individuellement est l’inconvénient de cette approche et l’utilisation de MutableMapping
(voir la réponse acceptée), mais ce n’est vraiment pas beaucoup plus de travail.
Tout d’abord, considérons la différence entre Python 2 et 3, créons un singleton (_RaiseKeyError
)) Pour nous assurer de savoir si nous obtenons réellement un argument pour dict.pop
, et crée 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 tous les arguments pour que ce code fonctionne avec 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 identique à celle utilisée pour toute méthode ou méthode spéciale faisant référence à une clé. Toutefois, 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 haskey
était obsolète 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)
Est-ce que j'empêche le marinage de fonctionner et dois-je implémenter
__setstate__
Etc.?
Et la sous-classe dict pickles va 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__
Est-ce que j'ai besoin de repr, update et
__init__
?
Nous avons défini update
et __init__
, Mais vous avez un beau __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 capacité de débogage 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 des traces:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Devrais-je simplement utiliser
mutablemapping
(il semble qu'on ne devrait pas utiliserUserDict
ouDictMixin
)? Si c'est le cas, comment? Les documents ne sont pas vraiment éclairants.
Oui, ce sont quelques lignes de code supplémentaires, mais elles sont conçues pour être complètes. Mon premier penchant serait d’utiliser la réponse acceptée, et si elle posait problème, j’examinerais ensuite ma réponse, car c’est un peu plus compliqué et qu’aucun ABC ne peut m'aider à comprendre correctement mon interface.
L'optimisation prématurée va de pair avec une plus grande complexité dans la recherche de performance. MutableMapping
est plus simple - il obtient donc un bord immédiat, toutes choses égales par ailleurs. Néanmoins, pour exposer toutes les différences, comparons-les.
Je devrais ajouter qu'il y avait une Push pour mettre un dictionnaire similaire dans le module collections
, mais il a été rejeté . Vous devriez probablement simplement faire ceci à la place:
my_dict[transform(key)]
Cela devrait être beaucoup plus facile à mettre au point.
Il y a 6 fonctions d'interface implémentées avec 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 ils sont assez simples, car je peux utiliser l'héritage pour la plupart de ces implémentations.
Le MutableMapping
implémente certaines choses dans Python que dict
implémente en C - donc je m'attendrais à ce qu'une sous-classe dict
soit plus performante dans certains cas.
Nous obtenons un __eq__
Libre dans les deux approches - les deux n'assumant l'égalité que si un autre dict est en minuscule - mais encore une fois, je pense que la sous-classe dict
se comparera plus rapidement.
MutableMapping
est plus simple avec moins d'opportunités pour les bogues, mais plus lente, prend plus de mémoire (voir dict redondant) et échoue isinstance(x, dict)
dict
est plus rapide, utilise moins de mémoire et transmet isinstance(x, dict)
, mais sa mise en œuvre est plus complexe.Lequel est le plus parfait? Cela dépend de votre définition de parfait.
Mes exigences étaient un peu plus strictes:
Ma pensée initiale était de substituer notre classe Path maladroite à une sous-classe unicode insensible à la casse - mais:
some_dict[CIstr(path)]
est moche)Donc, je devais enfin écrire ce dict insensible à la casse. Merci à 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 que la poussière est retombée, renommer les attributs/variables pour commencer par ci (et un gros commentaire de doc expliquant que ci signifie "insensible à la casse"). Je pense que c'est une solution parfaite - car les lecteurs du code doivent sachez que nous avons affaire à des structures de données sous-jacentes sensibles à la casse. J'espère que cela corrigera certains problèmes de reproduction, ce qui, je suppose, est dû à la sensibilité à la casse.
Commentaires/corrections bienvenus :)
Tout ce que vous aurez à faire c'est
class BatchCollection(dict):
def __init__(self, *args, **kwargs):
dict.__init__(*args, **kwargs)
OR
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 essayé les deux suggestions hautdeux , j'ai opté pour une voie moyenne assez sombre pour Python 2.7. Peut-être que 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 semble correspondre à mes besoins, qui sont:
**my_dict
dict
, , cela contourne votre code . Essaye le.isinstance(my_dict, dict)
dict
Si vous avez besoin de vous distinguer des autres, j'utilise personnellement 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 que de vous reconnaître en interne, il est ainsi plus difficile d'appeler accidentellement __am_i_me
En raison de la modification du nom de python (renommé _MyDict__am_i_me
À partir de tout appel en dehors de cette classe). Un peu plus privé que _method
, Tant dans la pratique que dans la culture.
Jusqu'à présent, je ne me plains pas, mis à part la dérogation sérieuse et louche __class__
. Je serais ravi d'entendre parler de tous les problèmes que d'autres rencontreraient avec cela, cependant, je ne comprends pas bien les conséquences. Mais jusqu'à présent, je n'ai rencontré aucun problème, ce qui m'a permis de migrer beaucoup de code de qualité moyenne dans de nombreux emplacements sans avoir besoin de modifications.
Comme preuve: https://repl.it/repls/TraumaticToughCockatoo
Fondamentalement: copy option # 2 actuelle , ajoutez des lignes print 'method_name'
À chaque méthode, puis essayez ceci et regardez le résultat:
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. Supposons que votre faux -dict
soit une enveloppe autour d'un autre type de données, il n'y a donc aucun moyen raisonnable de stocker les données dans le dicton de sauvegarde; **your_dict
Sera vide, quelle que soit la méthode utilisée.
Cela fonctionne correctement pour MutableMapping
, mais dès que vous héritez de dict
, il devient incontrôlable.