2011-06-10 16 views
15

consideran este código:Python: inconsistencia en la forma en que define la función __setattr__?

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) 

Yo esperaría que la misma salida. Sin embargo, con CPython 2.5, 2.6 (de manera similar en 3.2) me sale:

({'x': 42}, 42) 
({}, 42) 

Con PyPy 1.5.0, puedo obtener el resultado esperado:

({'x': 42}, 42) 
({'x': 42}, 42) 

que es la salida "derecho"? (O lo que debería ser la salida de acuerdo con la documentación de Python?)


Here es el informe de error para CPython.

+1

Interesante rompecabezas. Me pregunto si el comportamiento de CPython tiene algo que ver con 'dict .__ setitem__' siendo un * slot wrapper *. – NPE

Respuesta

7

Sospecho que tiene que ver con una optimización de las operaciones de búsqueda. Desde el 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); 
    } 

La ruta rápida no lo busca en el diccionario de la 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) 

Lo que me pareció funciona como se espera.

3

Es una diferencia documentada conocida (y tal vez no tan bien). PyPy no diferencia entre funciones y funciones integradas. En CPython, las funciones se combinan como métodos independientes cuando se almacenan en la clase (tienen __get__), mientras que las funciones integradas no (son diferentes).

En PyPy, sin embargo, las funciones integradas son exactamente las mismas que las funciones de python, por lo que el intérprete no puede diferenciarlas y las trata como funciones de nivel python. Creo que esto se definió como detalles de implementación, aunque hubo cierta discusión sobre el desarrollador de python sobre la eliminación de esta diferencia particular.

Saludos,
Fijal

1

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 sólo podemos envolverlos (y __getattr__ a trabajar 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 hacen resumido en libros minuciosos como 'Learning Python' o 'Python in a nutshell' y algo vagamente especificado en python.org) y se refiere a la parte del lenguaje que está documentada 'tal cual' por la implementación (y está sujeta a (más bien) cambios frecuentes).

Cuestiones relacionadas