Comment remplacer «parfaitement» un dict?

218

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.

Il semblerait qu'il devrait y avoir un petit ensemble de primitives que je peux remplacer pour que cela fonctionne, mais selon toutes mes recherches et tentatives, il semble que ce ne soit pas le cas:

  • Si je remplace __getitem__/__setitem__ , alors get/ setne fonctionne pas. Comment puis-je 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 repr, updateet__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.

Voici mon premier essai, get()cela ne fonctionne pas et il y a sans aucun doute de nombreux 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 
    # /programming/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()
Paul Biggar
la source
Je pense que __keytransform __ () devrait être statique. Belle approche cependant. (prepending @staticmethod)
Aiyion.Prime

Réponses:

229

Vous pouvez écrire un objet qui se comporte dictassez facilement avec les ABC (classes de base abstraites) du collections.abcmodule. Il vous indique même si vous avez manqué une méthode, voici donc la version minimale qui ferme l'ABC.

from collections.abc import MutableMapping


class TransformedDict(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
# works too since we just use a normal dict
assert pickle.loads(pickle.dumps(s)) == s

Je ne sous- dictclasserais pas (ou d'autres modules intégrés) directement. Cela n'a souvent aucun sens, car ce que vous voulez réellement faire, c'est implémenter l'interface de adict . Et c'est exactement à cela que servent les ABC.

Jochen Ritzel
la source
46
Je suggérerais de renommer __keytransform__()car il viole le guide de style PEP 8 qui conseille "Ne jamais inventer de tels noms; utilisez-les uniquement comme documenté" à la fin de la section Descriptive: Naming Styles .
martineau
1
Question cependant - l'implémentation de cette interface avec un type défini par l'utilisateur n'entraînera-t-elle généralement pas des opérations de type dict plus lentes qu'en utilisant le type intégré?
twneale
2
Existe-t-il un moyen de le faire pour que l'instance (_, dict) == True? Ou utilisez-vous simplement Mutable Mapping pour construire puis sous-classe?
Andy Hayden
5
@AndyHayden: Vous devriez écrire if isinstance(t, collections.MutableMapping): print t, "can be used like a dict". Ne vérifiez pas le type d'un objet, vérifiez l'interface.
Jochen Ritzel
2
@NeilG Cela inclut malheureusement le JSONEncoder dans la bibliothèque standard de python - github.com/python-git/python/blob/…
Andy Smith
97

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.popet 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 superavec 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, popitemet valuesgratuitement. 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 updateet __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. MutableMappingest 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 collectionsmodule, 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 dictsous - 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' MutableMappingimplémente certaines choses en Python qui dictimplémentent en C - donc je m'attends à ce qu'une dictsous - 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 dictsous - 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.

Aaron Hall
la source
Comment la réponse acceptée supprimerait-elle le dicton redondant?
Seanny123
1
Deux façons qui me viennent immédiatement à l'esprit sont de déclarer l'attribut de magasin dans __slots__ou peut-être de réutiliser le __dict__comme magasin, mais cela mélange la sémantique, un autre point de critique potentiel.
Aaron Hall
1
N'aurait-il pas été plus facile d'écrire un décorateur qui prend une méthode et utilise votre ensure_lowersur le premier argument (qui est toujours la clé)? Ce serait alors le même nombre de remplacements, mais ils seraient tous de la forme __getitem__ = ensure_lower_decorator(super(LowerDict, self).__getitem__).
Graipher
1
Merci pour cela - obtenir des avertissements pour pop et fromkeys qu'ils ne correspondent pas à la signature de la méthode de classe de base.
Mr_and_Mrs_D
1
@Mr_and_Mrs_D J'ai ajouté une implémentation de copy- je pense que cela devrait le faire, non? Je pense qu'il devrait tester l'interface - par exemple, l'objet DataFrame pandas n'est pas une instance de Mapping (au dernier contrôle) mais il a des éléments / iteritems.
Aaron Hall
4

Mes exigences étaient un peu plus strictes:

  • J'ai dû conserver les informations de casse (les chaînes sont des chemins d'accès aux 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 que les clés soient aussi petites que possible (cela faisait une différence dans les performances de la mémoire, coupé à 110 Mo sur 370). Cela signifiait que la mise en cache de la version minuscule des clés n'est pas une option.
  • J'avais besoin que la création des structures de données soit aussi rapide que possible (encore une fois, cela a fait une différence dans les performances, la vitesse). 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 de bien faire les choses - voir: Une classe de chaîne insensible à la casse en python
  • s'avère que la gestion explicite des clés dict rend le code verbeux et désordonné - et sujet aux erreurs (les structures sont transmises 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)] laid)

J'ai donc finalement dû écrire ce dict insensible à la casse. Merci 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 par ci (et un gros commentaire de doc expliquant que ci signifie insensible à la casse), je pense que c'est une solution parfaite - comme les lecteurs du code doivent sachez que nous avons affaire à des structures de données sous-jacentes insensibles à la casse. J'espère que cela corrigera certains bogues difficiles à reproduire, qui, je suppose, se résument à la sensibilité à la casse.

Commentaires / corrections bienvenus :)

Mr_and_Mrs_D
la source
Les CIstr __repr__devraient utiliser les classes parentes __repr__pour passer le test eval (repr (obj)) == obj (je ne pense pas que ce soit le cas actuellement) et ne pas se fier à __str__.
Aaron Hall
Consultez également le total_orderingdécorateur de classe - qui éliminera 4 méthodes de votre sous-classe unicode. Mais la sous-classe dict semble très intelligemment mise en œuvre. : P
Aaron Hall
Merci @AaronHall - c'est vous qui avez implémenté cela: P Re: commande totale - J'ai intentionnellement écrit les méthodes en ligne comme conseillé par Raymond Hettinger ici: stackoverflow.com/a/43122305/281545 . Re: repr: Je me souviens avoir lu un commentaire (par un développeur principal de l'IIRC) qui, bien, ne vaut pas vraiment la peine d'essayer de faire passer la repr à ce test (c'est un problème) - mieux se concentrer sur le fait qu'il soit aussi informatif que possible ( mais pas plus)
Mr_and_Mrs_D
Je vais vous permettre vos méthodes de comparaison redondantes (vous devriez en faire une note dans votre réponse), mais le CIstr.__repr__, dans votre cas, peut passer le test de repr avec très peu de tracas, et cela devrait rendre le débogage beaucoup plus agréable. J'ajouterais également un __repr__pour votre dict. Je vais le faire dans ma réponse pour le démontrer.
Aaron Hall
@AaronHall: J'ai ajouté __slots__dans CIstr - cela fait une différence dans les performances (CIstr n'est pas censé être sous-classé ni utilisé en dehors de LowerDict, devrait être une classe finale imbriquée statique). Toujours pas sûr comment résoudre le problème de manière élégante rééd (la piqûre peut contenir une combinaison 'et "citations)
Mr_and_Mrs_D
4

Tout ce que vous aurez à faire, c'est

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

Remarque : testé uniquement en python3

ravi404
la source
3

Après avoir essayé les deux premiers deux suggestions, je me suis installé sur une route moyenne louche à la recherche de 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
   @property
   def __class__(self):
       return dict

que je déteste vraiment, mais qui semble correspondre à mes besoins, à savoir:

  • peut remplacer **my_dict
    • si vous héritez de dict, cela contourne votre code . Essaye le.
    • cela rend le # 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 ne suffit pas
    • Je recommande chaleureusement # 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 devez vous distinguer des autres, personnellement, j'utilise quelque chose comme ça (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, de cette façon, il est plus difficile d'appeler accidentellement en __am_i_meraison du munging de nom de python (cela est renommé _MyDict__am_i_mede tout ce qui appelle en dehors de cette classe). Un peu plus privé que _methods, à la fois dans la pratique et sur le plan culturel.

Jusqu'à présent, je n'ai rien à redire, à part la __class__dérogation à l' ombre sérieuse . Je serais ravi d'entendre parler des problèmes que d'autres rencontrent avec cela, je ne comprends pas complètement les conséquences. Mais jusqu'à présent, je n'ai eu aucun problème, ce qui m'a permis de migrer beaucoup de code de qualité intermédiaire dans de nombreux endroits sans avoir besoin de changements.


Comme preuve: https://repl.it/repls/TraumaticToughCockatoo

Fondamentalement: copiez l'option # 2 actuelle , ajoutez des print 'method_name'lignes à chaque méthode, puis essayez ceci et regardez 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. Disons que votre faux dictest un wrapper autour d'un autre type de données, il n'y a donc pas de moyen raisonnable de stocker les données dans le dicté; **your_dictsera vide, quelles que soient les autres méthodes.

Cela fonctionne correctement pour MutableMapping, mais dès que vous en héritez dictdevient incontrôlable.


Edit: en tant que mise à jour, cela fonctionne sans problème depuis près de deux ans maintenant, sur plusieurs centaines de milliers (eh, peut-être quelques millions) de lignes de python complexes héritées. Je suis donc assez content avec ça :)

Edit 2: apparemment, j'ai mal copié ceci ou quelque chose il y a longtemps. @classmethod __class__ne fonctionne pas pour les isinstancecontrôles - @property __class__fonctionne: https://repl.it/repls/UnitedScientificSequence

Groxx
la source
Qu'entendez-vous exactement par « **your_dictsera vide» (si vous sous-classe de dict)? Je n'ai vu aucun problème avec le déballage de dict ...
Matt P
Si vous placez des données dans le dict parent (comme le fait LowerDict), cela fonctionne - vous obtiendrez ces données stockées dans le dict. Si vous ne le faites pas (disons que vous vouliez générer des données à la volée, comme {access_count: "trace de l'accès à la pile"} qui se remplit chaque fois qu'il est lu), vous remarquerez que **your_dictcela n'exécute pas votre code, donc il ne peut rien produire de "spécial". Par exemple, vous ne pouvez pas compter les «lectures» car il n'exécute pas votre code de lecture. MutableMapping fait le travail pour cela (utiliser si vous le pouvez!), Mais il ne isinstance(..., dict)donc je ne pouvais pas l' utiliser. yay logiciel hérité.
Groxx
Ok, je vois ce que tu veux dire maintenant. Je suppose que je ne m'attendais pas à l'exécution de code avec **your_dict, mais je trouve très intéressant que MutableMappingcela fasse cela.
Matt P
Oui. Il est nécessaire pour un certain nombre de choses (par exemple, je shimait les appels RPC dans ce qui était auparavant une lecture locale, et je devais le faire à la demande pour Reasons ™), et il semble que très peu de gens le sachent, même si **some_dictest assez courant. À tout le moins , il arrive très souvent dans les décorateurs, donc si vous avez une , vous êtes immédiatement au risque de mauvaise conduite en apparence impossible si vous ne prennent pas en compte pour elle.
Groxx
Peut-être que je manque quelque chose, mais l' def __class__()astuce ne semble pas fonctionner avec Python 2 ou 3, au moins pour l'exemple de code dans la question Comment enregistrer l'implémentation d'abc.MutableMapping en tant que sous-classe dict?(modifié pour fonctionner autrement dans les deux versions). Je veux isinstance(SpreadSheet(), dict)revenir True.
martineau