51 votes

Sous-classement d'un dictionnaire Python pour remplacer __setitem__

Je construis une classe qui sous-classe dict et remplace __setitem__ . Je voudrais être certain que ma méthode sera appelée dans tous les cas où des éléments du dictionnaire pourraient être définis.

J'ai découvert trois situations dans lesquelles Python (dans ce cas, 2.6.4) n'appelle pas ma fonction surchargée __setitem__ lors de la définition des valeurs, et appelle à la place la méthode PyDict_SetItem directement

  1. Dans le constructeur
  2. Dans le setdefault méthode
  3. Dans le update méthode

Il s'agit d'un test très simple :

class MyDict(dict):
    def __setitem__(self, key, value):
        print "Here"
        super(MyDict, self).__setitem__(key, str(value).upper())

>>> a = MyDict(abc=123)
>>> a['def'] = 234
Here
>>> a.update({'ghi': 345})
>>> a.setdefault('jkl', 456)
456
>>> print a
{'jkl': 456, 'abc': 123, 'ghi': 345, 'def': '234'}

Vous pouvez voir que la méthode surchargée n'est appelée que lorsque les éléments sont définis explicitement. Pour que Python appelle toujours ma méthode __setitem__ j'ai dû réimplémenter ces trois méthodes, comme ceci :

class MyUpdateDict(dict):
    def __init__(self, *args, **kwargs):
        self.update(*args, **kwargs)

    def __setitem__(self, key, value):
        print "Here"
        super(MyUpdateDict, self).__setitem__(key, value)

    def update(self, *args, **kwargs):
        if args:
            if len(args) > 1:
                raise TypeError("update expected at most 1 arguments, got %d" % len(args))
            other = dict(args[0])
            for key in other:
                self[key] = other[key]
        for key in kwargs:
            self[key] = kwargs[key]

    def setdefault(self, key, value=None):
        if key not in self:
            self[key] = value
        return self[key]

Existe-t-il d'autres méthodes que je dois surcharger, afin de savoir que Python va toujours appeler mon __setitem__ méthode ?

UPDATE

Selon la suggestion de gs, j'ai essayé de sous-classer UserDict (en fait, IterableUserDict, puisque je veux itérer sur les clés) comme ceci :

from UserDict import *;
class MyUserDict(IterableUserDict):
    def __init__(self, *args, **kwargs):
        UserDict.__init__(self,*args,**kwargs)

    def __setitem__(self, key, value):
        print "Here"
        UserDict.__setitem__(self,key, value)

Cette classe semble appeler correctement mon __setitem__ sur setdefault mais il ne l'appelle pas sur update ou lorsque les données initiales sont fournies au constructeur.

MISE À JOUR 2

La suggestion de Peter Hansen m'a poussé à regarder plus attentivement le fichier dictobject.c, et j'ai réalisé que la méthode de mise à jour pouvait être simplifiée un peu, puisque le constructeur du dictionnaire intégré appelle simplement la méthode de mise à jour intégrée de toute façon. Cela ressemble maintenant à ceci :

def update(self, *args, **kwargs):
    if len(args) > 1:
        raise TypeError("update expected at most 1 arguments, got %d" % len(args))
    other = dict(*args, **kwargs)
    for key in other:
        self[key] = other[key]

54voto

Ian Clelland Points 15066

Je réponds à ma propre question, puisque j'ai finalement décidé que j'avais vraiment faire veulent sous-classer Dict, plutôt que de créer une nouvelle classe de mappage, et UserDict s'en remet toujours à l'objet Dict sous-jacent dans certains cas, plutôt que d'utiliser la classe de mappage fournie. __setitem__ .

Après avoir lu et relu les sources de Python 2.6.4 (principalement Objects/dictobject.c mais j'ai cherché partout ailleurs pour voir où les différentes méthodes sont utilisées), j'ai cru comprendre que le code suivant es suffisant pour que mon __setitem__ soit appelé à chaque fois que l'objet est modifié, et pour qu'il se comporte exactement comme un Dict Python :

La suggestion de Peter Hansen m'a amené à examiner plus attentivement dictobject.c J'ai réalisé que la méthode de mise à jour de ma réponse originale pouvait être simplifiée un peu, puisque le constructeur du dictionnaire intégré appelle simplement la méthode de mise à jour intégrée de toute façon. La deuxième mise à jour de ma réponse a donc été ajoutée au code ci-dessous (par une personne serviable ;-).

class MyUpdateDict(dict):
    def __init__(self, *args, **kwargs):
        self.update(*args, **kwargs)

    def __setitem__(self, key, value):
        # optional processing here
        super(MyUpdateDict, self).__setitem__(key, value)

    def update(self, *args, **kwargs):
        if args:
            if len(args) > 1:
                raise TypeError("update expected at most 1 arguments, "
                                "got %d" % len(args))
            other = dict(args[0])
            for key in other:
                self[key] = other[key]
        for key in kwargs:
            self[key] = kwargs[key]

    def setdefault(self, key, value=None):
        if key not in self:
            self[key] = value
        return self[key]

Je l'ai testé avec ce code :

def test_updates(dictish):
    dictish['abc'] = 123
    dictish.update({'def': 234})
    dictish.update(red=1, blue=2)
    dictish.update([('orange', 3), ('green',4)])
    dictish.update({'hello': 'kitty'}, black='white')
    dictish.update({'yellow': 5}, yellow=6)
    dictish.setdefault('brown',7)
    dictish.setdefault('pink')
    try:
        dictish.update({'gold': 8}, [('purple', 9)], silver=10)
    except TypeError:
        pass
    else:
        raise RunTimeException("Error did not occur as planned")

python_dict = dict([('b',2),('c',3)],a=1)
test_updates(python_dict)

my_dict = MyUpdateDict([('b',2),('c',3)],a=1)
test_updates(my_dict)

et ça passe. Toutes les autres implémentations que j'ai essayées ont échoué à un moment ou à un autre. J'accepterai toujours les réponses qui me montrent que j'ai manqué quelque chose, mais sinon, je cocherai la case à côté de celle-ci dans quelques jours, et je dirai que c'est la bonne réponse :)

4voto

rizac Points 11

J'ai trouvé la réponse et les commentaires de Ian très utiles et clairs. Je voudrais juste faire remarquer que peut-être un premier appel à la super-classe __init__ pourrait être plus sûr, alors qu'il n'est pas nécessaire : J'ai récemment eu besoin d'implémenter une OrderedDict (Je travaille avec Python 2.7) : après avoir implémenté et modifié mon code en fonction de la méthode proposée MyUpdateDict j'ai découvert qu'en remplaçant simplement

class MyUpdateDict(dict):

avec :

from collections import OrderedDict
class MyUpdateDict(OrderedDict):

alors le code de test posté ci-dessus a échoué :

Traceback (most recent call last):
File "Desktop/test_updates.py", line 52, in <module>
    my_dict = MyUpdateDict([('b',2),('c',3)],a=1)
File "Desktop/test_updates.py", line 5, in __init__
    self.update(*args, **kwargs)
File "Desktop/test_updates.py", line 18, in update
    self[key] = other[key]
File "Desktop/test_updates.py", line 9, in __setitem__
    super(MyUpdateDict, self).__setitem__(key, value)
File "/usr/lib/python2.7/collections.py", line 59, in __setitem__
    root = self.__root
AttributeError: 'MyUpdateDict' object has no attribute '_OrderedDict__root'

Regarder code collections.py il s'avère que le OrderedDict besoins son __init__ à appeler afin d'initialiser et de configurer les attributs personnalisés nécessaires.

Par conséquent, en ajoutant simplement un premier appel à la fonction super __init__ méthode,

from collections import OrderedDict
class MyUpdateDict(Orderedict):
def __init__(self, *args, **kwargs):
    super(MyUpdateDict, self).__init__() #<-- HERE call to super __init__
    self.update(*args, **kwargs)

nous disposons d'une solution plus générale qui fonctionne apparemment à la fois pour dict et OrderedDict.

Je ne peux pas dire si cette solution est valable en général, car je l'ai testée uniquement avec OrderedDict. Cependant, il est probable qu'un appel à la fonction super __init__ est soit inoffensif, soit nécessaire plutôt que nuisible, lorsqu'on essaie d'étendre d'autres sous-classes de dictées

-1voto

Utiliser object.keyname = value au lieu de object["keyname"] = value

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X