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__ , puis get / 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 ou DictMixin )? 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()
159
demandé sur Bonifacio2 2010-08-02 16:23:28

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.

171
répondu Jochen Ritzel 2013-10-02 11:44:48

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 utiliser UserDict ou DictMixin )? 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 utiliser UserDict ou DictMixin )? 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 échoue isinstance(x, dict)
  • sous-classement dict est plus rapide, utilise moins de mémoire, et passe isinstance(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.

58
répondu Aaron Hall 2017-07-23 12:45:18

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

2
répondu Mr_and_Mrs_D 2017-05-23 12:26:19

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

1
répondu ravi404 2017-10-06 08:06:06

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
  • mascarades isinstance(my_dict, dict)
    • exclut le MutableMapping seul, donc #1 n'est pas suffisant
    • je vous recommande vivement #1 si vous n'avez pas besoin de ceci, 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, 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.

0
répondu Groxx 2017-11-18 02:01:07