Python: inconsistance dans la façon dont vous définissez la fonction __setattr__?
Question
Considérez ce code:
class Foo1(dict):
def __getattr__(self, key): return self[key]
def __setattr__(self, key, value): self[key] = value
class Foo2(dict):
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
o1 = Foo1()
o1.x = 42
print(o1, o1.x)
o2 = Foo2()
o2.x = 42
print(o2, o2.x)
J'attendre la même sortie. Cependant, avec CPython 2.5, 2.6 (de la même 3.2) je reçois:
({'x': 42}, 42)
({}, 42)
Avec PyPy 1.5.0, je reçois le résultat attendu:
({'x': 42}, 42)
({'x': 42}, 42)
Quelle est la sortie « droit »? (Ou quelle devrait être la sortie en fonction de la documentation Python?)
La solution
Je soupçonne qu'il a à faire avec une optimisation de la recherche. À partir du code source:
/* speed hack: we could use lookup_maybe, but that would resolve the
method fully for each attribute lookup for classes with
__getattr__, even when the attribute is present. So we use
_PyType_Lookup and create the method only when needed, with
call_attribute. */
getattr = _PyType_Lookup(tp, getattr_str);
if (getattr == NULL) {
/* No __getattr__ hook: use a simpler dispatcher */
tp->tp_getattro = slot_tp_getattro;
return slot_tp_getattro(self, name);
}
Le chemin rapide ne ne regarde pas sur le dictionnaire de classe.
Par conséquent, la meilleure façon d'obtenir la fonctionnalité souhaitée est de placer une méthode de remplacement dans la classe.
class AttrDict(dict):
"""A dictionary with attribute-style access. It maps attribute access to
the real dictionary. """
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, dict.__repr__(self))
def __setitem__(self, key, value):
return super(AttrDict, self).__setitem__(key, value)
def __getitem__(self, name):
return super(AttrDict, self).__getitem__(name)
def __delitem__(self, name):
return super(AttrDict, self).__delitem__(name)
__getattr__ = __getitem__
__setattr__ = __setitem__
def copy(self):
return AttrDict(self)
Ce que je trouve fonctionne comme prévu.
Autres conseils
Il est un connu (et peut-être pas si bien) différence documentée. PyPy ne fait pas de distinction entre les fonctions et les fonctions builtin. Dans les fonctions CPython se binded comme méthodes non liées lorsqu'elles sont stockées sur la classe (l'ont __get__), tandis que les fonctions builtin ne pas (ils sont différents).
Dans le cadre PyPy Cependant, les fonctions sont builtin exactement les mêmes que les fonctions de python, de sorte que l'interprète ne peut pas leur dire d'eux à part et traite comme des fonctions de niveau python. Je pense que cela a été défini comme les détails de mise en œuvre, bien qu'il y ait eu des discussions sur python-dev sur la suppression de cette différence particulière.
Cheers,
Fijal
Notez les points suivants:
>>> dict.__getitem__ # it's a 'method'
<method '__getitem__' of 'dict' objects>
>>> dict.__setitem__ # it's a 'slot wrapper'
<slot wrapper '__setitem__' of 'dict' objects>
>>> id(dict.__dict__['__getitem__']) == id(dict.__getitem__) # no bounding here
True
>>> id(dict.__dict__['__setitem__']) == id(dict.__setitem__) # or here either
True
>>> d = {}
>>> dict.__setitem__(d, 1, 2) # can be called directly (since not bound)
>>> dict.__getitem__(d, 1) # same with this
2
Maintenant, nous pouvons simplement les envelopper (et __getattr__
travaillerons même sans cela):
class Foo1(dict):
def __getattr__(self, key): return self[key]
def __setattr__(self, key, value): self[key] = value
class Foo2(dict):
"""
It seems, 'slot wrappers' are not bound when present in the __dict__
of a class and retrieved from it via instance (or class either).
But 'methods' are, hence simple assignment works with __setitem__
in your original example.
"""
__setattr__ = lambda *args: dict.__setitem__(*args)
__getattr__ = lambda *args: dict.__getitem__(*args) # for uniformity, or
#__getattr__ = dict.__getitem__ # this way, i.e. directly
o1 = Foo1()
o1.x = 42
print(o1, o1.x)
o2 = Foo2()
o2.x = 42
print(o2, o2.x)
Ce qui donne:
>>>
({'x': 42}, 42)
({'x': 42}, 42)
Le mécanisme derrière le comportement en question est (probablement, je ne suis pas un expert) en dehors du sous-ensemble « propre » de Python (comme documenté dans les livres complets comme « Learning Python » ou « Python en un mot » et un peu vaguement spécifié à python.org) et se rapporte à la partie de la langue qui est décrite « en l'état » par la mise en œuvre (et est soumise à (plutôt) des changements fréquents).