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.

¿Fue útil?

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top