web-dev-qa-db-fra.com

Sous-classement Python pour remplacer __setitem__

Je crée 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 de dictionnaire pourraient éventuellement être définis.

J'ai découvert trois situations où Python (dans ce cas, 2.6.4) n'appelle pas mon _ __setitem__ méthode lors de la définition des valeurs et appelle à la place PyDict_SetItem directement

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

Comme 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 substituée n'est appelée que lors de la définition explicite des éléments. Pour obtenir Python pour toujours appeler mon __setitem__ méthode, 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 remplacer, afin de savoir que Python va toujours appeler mon __setitem__ méthode?

MISE À JOUR

Selon la suggestion de GS, j'ai essayé de sous-classer UserDict (en fait, IterableUserDict, car je veux parcourir 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 amené à examiner plus attentivement dictobject.c, et j'ai réalisé que la méthode de mise à jour pourrait être un peu simplifiée, car le constructeur de 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]
44
Ian Clelland

Je réponds à ma propre question, car j'ai finalement décidé que je voulais vraiment faire sous-classer Dict, plutôt que de créer une nouvelle classe de mappage, et UserDict se réfère toujours à l'objet Dict sous-jacent dans certains cas, plutôt que d'utiliser le __setitem__ fourni.

Après avoir lu et relu la source 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)), ma compréhension est que le le code suivant est suffisant pour que mon __setitem__ soit appelé à chaque fois que l'objet est modifié, et pour se comporter exactement comme un Python Dict:

La suggestion de Peter Hansen m'a amené à examiner plus attentivement dictobject.c, Et j'ai réalisé que la méthode de mise à jour dans ma réponse d'origine pourrait être un peu simplifiée, puisque le constructeur de 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 utile ;-).

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 donné. J'accepterai toujours toutes les réponses qui me montreront que j'ai raté quelque chose, mais sinon, je coche la coche à côté de celle-ci dans quelques jours et je l'appelle la bonne réponse :)

50
Ian Clelland

Quel est votre cas d'utilisation pour le sous-classement dict?

Vous n'avez pas besoin de le faire pour implémenter un objet de type dict, et il pourrait être plus simple dans votre cas d'écrire une classe ordinaire, puis d'ajouter la prise en charge du sous-ensemble requis de l'interface dict.

La meilleure façon d'accomplir ce que vous recherchez est probablement la classe de base abstraite MutableMapping. PEP 3119 - Présentation des classes de base abstraites

Cela vous aidera également à répondre à la question "Y a-t-il d'autres méthodes que je dois remplacer?". Vous devrez remplacer toutes les méthodes abstraites. Pour MutableMapping: Les méthodes abstraites incluent setitem , delitem . Les méthodes concrètes incluent pop, popitem, clear, update.

4
mluebke

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

class MyUpdateDict(dict):

avec:

from collections import OrderedDict
class MyUpdateDict(OrderedDict):

puis le code de test affiché 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'

En regardant code collections.py il s'avère que OrderedDict a besoin de son __init__ méthode à appeler pour initialiser et configurer les attributs personnalisés nécessaires.

Par conséquent, en ajoutant simplement un premier appel au 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 avons une solution plus générale qui fonctionne apparemment pour dict et OrderedDict.

Je ne peux pas dire si cette solution est généralement valide, car je l'ai testée avec OrderedDict uniquement. Cependant, il est probable qu'un appel au super __init__ la méthode est soit inoffensive, soit nécessaire plutôt que nuisible, lorsqu'on essaie d'étendre d'autres sous-classes dict

3
rizac

Utilisez object.key name = value au lieu de object ["key name"] = value

0
Shahul Hameed P