Python: Inconsistencia en la forma en que define la función __setattr__?
Pregunta
Considere este código:
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)
Esperaría la misma salida. Sin embargo, con Cpython 2.5, 2.6 (de manera similar en 3.2) obtengo:
({'x': 42}, 42)
({}, 42)
Con Pypy 1.5.0, obtengo la salida esperada:
({'x': 42}, 42)
({'x': 42}, 42)
¿Cuál es la salida "correcta"? (¿O cuál debería ser la salida de acuerdo con la documentación de Python?)
Aquí es el informe de errores para CPython.
Solución
Sospecho que tiene que ver con una optimización de búsqueda. Del código fuente:
/* 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);
}
El camino rápido no lo busca en el diccionario de clase.
Por lo tanto, la mejor manera de obtener la funcionalidad deseada es colocar un método de anulación en la clase.
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)
Que encontré funciona como se esperaba.
Otros consejos
Es una diferencia documentada conocida (y tal vez no tan bien). Pypy no diferencia entre funciones y funciones construidas. En las funciones de CPython, se unen como métodos no unidos cuando se almacenan en la clase (have __get__), mientras que las funciones integradas no (son diferentes).
Sin embargo, bajo Pypy, las funciones Builtin son exactamente las mismas que las funciones de Python, por lo que el intérprete no puede distinguirlos y los trata como funciones a nivel de pitón. Creo que esto se definió como detalles de implementación, aunque hubo cierta discusión sobre Python-Dev sobre eliminar esta diferencia particular.
Salud,
fijal
Tenga en cuenta lo siguiente:
>>> 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
Ahora podemos envolverlos (y __getattr__
funcionará incluso sin eso):
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)
Lo que da:
>>>
({'x': 42}, 42)
({'x': 42}, 42)
El mecanismo detrás del comportamiento en cuestión es (probablemente, no soy un experto) fuera del subconjunto 'limpio' de Python (como se documenta en libros completos como 'Learning Python' o 'Python en pocas palabras' y algo libremente especificado en Python. org) y se refiere a la parte del lenguaje que está documentado 'tal cual es' por la implementación (y está sujeta a (más bien) cambios frecuentes).