web-dev-qa-db-fra.com

Comment "parfaitement" écraser un dict?

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()
194
Paul Biggar

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.

201
Jochen Ritzel

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 utiliser UserDict ou DictMixin)? 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.

Quel est le problème avec la réponse acceptée?

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:

  • La réponse acceptée manque également à la méthode de classe: 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'}}
    

En fait, sous-classer 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.?

pickling

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})

Conclusion

Devrais-je simplement utiliser 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.

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.

Compare et nuance

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.

Sommaire:

  • la sous-classe 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)
  • la sous-classe 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.

83
Aaron Hall

Mes exigences étaient un peu plus strictes:

  • Je devais conserver les informations de cas (les chaînes sont des chemins d'accès aux fichiers affichés à l'utilisateur, mais c'est une application Windows, donc toutes les opérations doivent être sensibles à la casse en interne)
  • J'avais besoin de touches aussi petites que possible (cela a effectivement une incidence sur les performances de la mémoire, coupé 110 mb sur 370). Cela signifiait que la mise en cache de la version minuscule des clés n'était pas une option.
  • J'avais besoin de la création des structures de données pour être aussi rapide que possible (encore une fois fait une différence dans les performances, la vitesse cette fois). Je devais y aller avec un intégré

Ma pensée initiale était de substituer notre classe Path maladroite à une sous-classe unicode insensible à la casse - mais:

  • s'est avéré difficile à comprendre - voir: ne classe de chaînes insensible à la casse en python
  • s’avère que la manipulation de clés dict explicites rend le code verbeux et compliqué - et propice aux erreurs (les structures sont passées ici et là, et il n’est pas clair si elles ont des instances CIStr comme clés/éléments, faciles à oublier plus 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 :)

4
Mr_and_Mrs_D

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

3
ravi404

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:

  • peut remplacer **my_dict
    • si vous héritez de dict, , cela contourne votre code . Essaye le.
    • cela rend # 2 inacceptable pour moi à tout moment , car c'est assez courant dans le code python
  • se fait passer pour isinstance(my_dict, dict)
    • exclut MutableMapping seul, donc # 1 n'est pas suffisant
    • Je recommande vivement # 1 si vous n'en avez pas besoin, c'est simple et prévisible
  • comportement entièrement contrôlable
    • donc je ne peux pas hériter de 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.

2
Groxx