ylab color change python inheritance properties mutators

color - Diseño de descriptor de propiedad de Python: ¿por qué copiar en lugar de mutar?



python plotly axis (3)

Estaba viendo cómo Python implementa el descriptor de propiedad internamente. De acuerdo con la property() docs property() se implementa en términos del protocolo descriptor, reproduciéndolo aquí por conveniencia:

class Property(object): "Emulate PyProperty_Type() in Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError("unreadable attribute") return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError("can''t set attribute") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError("can''t delete attribute") self.fdel(obj) def getter(self, fget): return type(self)(fget, self.fset, self.fdel, self.__doc__) def setter(self, fset): return type(self)(self.fget, fset, self.fdel, self.__doc__) def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel, self.__doc__)

Mi pregunta es: ¿por qué los últimos tres métodos no se implementan de la siguiente manera?

def getter(self, fget): self.fget = fget return self def setter(self, fset): self.fset = fset return self def deleter(self, fdel): self.fdel= fdel return self

¿Hay alguna razón para retomar nuevas instancias de propiedad, señalando internamente básicamente las mismas funciones de obtención y configuración?


Comencemos con un poco de historia, porque la implementación original había sido equivalente a su alternativa (equivalente porque la property se implementa en C en CPython para que el getter , etc. se escriba en C, no "plain Python").

Sin embargo, se informó como problema (1620) en el rastreador de errores de Python en 2007:

Como informó Duncan Booth en http://permalink.gmane.org/gmane.comp.python.general/551183 la nueva sintaxis @ spam.getter modifica la propiedad en su lugar, pero debería crear una nueva.

El parche es el primer borrador de una solución. Tengo que escribir pruebas unitarias para verificar el parche. Copia la propiedad y, como bonificación, toma la cadena __doc__ del getter si la cadena de documentación inicialmente también proviene del getter.

Lamentablemente, el enlace no va a ninguna parte (realmente no sé por qué se llama un "enlace permanente" ...). Se clasificó como error y se cambió a la forma actual (consulte este parche o la correspondiente confirmación de Github (pero es una combinación de varios parches) ). En caso de que no quiera seguir el enlace, el cambio fue:

PyObject * property_getter(PyObject *self, PyObject *getter) { - Py_XDECREF(((propertyobject *)self)->prop_get); - if (getter == Py_None) - getter = NULL; - Py_XINCREF(getter); - ((propertyobject *)self)->prop_get = getter; - Py_INCREF(self); - return self; + return property_copy(self, getter, NULL, NULL, NULL); }

Y similar para setter y deleter . Si no conoce C, las líneas importantes son:

((propertyobject *)self)->prop_get = getter;

y

return self;

el resto es principalmente "texto estándar de Python C API". Sin embargo, estas dos líneas son equivalentes a tu:

self.fget = fget return self

Y fue cambiado a:

return property_copy(self, getter, NULL, NULL, NULL);

que esencialmente hace:

return type(self)(fget, self.fset, self.fdel, self.__doc__)

¿Por qué fue cambiado?

Dado que el enlace está desactivado, no sé la razón exacta, sin embargo, puedo especular en función de los casos de prueba agregados en esa confirmación :

import unittest class PropertyBase(Exception): pass class PropertyGet(PropertyBase): pass class PropertySet(PropertyBase): pass class PropertyDel(PropertyBase): pass class BaseClass(object): def __init__(self): self._spam = 5 @property def spam(self): """BaseClass.getter""" return self._spam @spam.setter def spam(self, value): self._spam = value @spam.deleter def spam(self): del self._spam class SubClass(BaseClass): @BaseClass.spam.getter def spam(self): """SubClass.getter""" raise PropertyGet(self._spam) @spam.setter def spam(self, value): raise PropertySet(self._spam) @spam.deleter def spam(self): raise PropertyDel(self._spam) class PropertyTests(unittest.TestCase): def test_property_decorator_baseclass(self): # see #1620 base = BaseClass() self.assertEqual(base.spam, 5) self.assertEqual(base._spam, 5) base.spam = 10 self.assertEqual(base.spam, 10) self.assertEqual(base._spam, 10) delattr(base, "spam") self.assert_(not hasattr(base, "spam")) self.assert_(not hasattr(base, "_spam")) base.spam = 20 self.assertEqual(base.spam, 20) self.assertEqual(base._spam, 20) self.assertEqual(base.__class__.spam.__doc__, "BaseClass.getter") def test_property_decorator_subclass(self): # see #1620 sub = SubClass() self.assertRaises(PropertyGet, getattr, sub, "spam") self.assertRaises(PropertySet, setattr, sub, "spam", None) self.assertRaises(PropertyDel, delattr, sub, "spam") self.assertEqual(sub.__class__.spam.__doc__, "SubClass.getter")

Eso es similar a los ejemplos que ya dieron las otras respuestas. El problema es que desea poder cambiar el comportamiento en una subclase sin afectar la clase principal:

>>> b = BaseClass() >>> b.spam 5

Sin embargo, con su propiedad resultaría en esto:

>>> b = BaseClass() >>> b.spam --------------------------------------------------------------------------- PropertyGet Traceback (most recent call last) PropertyGet: 5

Eso sucede porque BaseClass.spam.getter (que se utiliza en SubClass ) en realidad modifica y devuelve la propiedad BaseClass.spam !

Así que sí, se ha cambiado (muy probablemente) porque permite modificar el comportamiento de una propiedad en una subclase sin cambiar el comportamiento en la clase principal.

Otra razón (?)

Tenga en cuenta que hay un motivo adicional, que es un poco tonto, pero que realmente vale la pena mencionar (en mi opinión):

Repasemos brevemente: Un decorador es solo azúcar sintáctico para una tarea, entonces:

@decorator def decoratee(): pass

es equivalente a:

def func(): pass decoratee = decorator(func) del func

El punto importante aquí es que el resultado del decorador se asigna al nombre de la función decorada . Por lo tanto, aunque generalmente utiliza el mismo "nombre de función" para getter / setter / deleter, ¡no es necesario!

Por ejemplo:

class Fun(object): @property def a(self): return self._a @a.setter def b(self, value): self._a = value >>> o = Fun() >>> o.b = 100 >>> o.a 100 >>> o.b 100 >>> o.a = 100 AttributeError: can''t set attribute

En este ejemplo, utiliza el descriptor para a para crear otro descriptor para b que se comporta como a excepto que obtuvo un setter .

Es un ejemplo bastante extraño y probablemente no se use con mucha frecuencia (o en absoluto). Pero incluso si es bastante extraño y (para mí) el estilo no es muy bueno, debe ilustrar que solo porque usa property_name.setter (o getter / deleter ) debe estar vinculado a property_name . ¡Podría estar ligado a cualquier nombre! Y no esperaría que se propague nuevamente a la propiedad original (aunque no estoy muy seguro de lo que esperaría aquí).

Resumen

  • CPython realmente utilizó el enfoque "modificar y devolver self " en el getter , setter y deleter una vez.
  • Había sido cambiado debido a un informe de error.
  • Se comportó "con errores" cuando se usaba con una subclase que sobrescribía una propiedad de la clase principal.
  • De manera más general: los decoradores no pueden influir en el nombre con el que estarán vinculados, por lo que es dudoso suponer que siempre es válido return self en un decorador (para un decorador de propósito general).

Entonces, ¿puedes usar propiedades con herencia?

Solo un intento de responder dando un ejemplo:

class Base(object): def __init__(self): self._value = 0 @property def value(self): return self._value @value.setter def value(self, val): self._value = val class Child(Base): def __init__(self): super().__init__() self._double = 0 @Base.value.setter def value(self, val): Base.value.fset(self, val) self._double = val * 2

Si se implementó de la manera en que lo escribe, entonces Base.value.setter también establecería el doble, lo cual no es Base.value.setter . Queremos un nuevo setter, no modificar el base.

EDITAR: como lo señaló @wim, en este caso particular, no solo modificaría al establecedor de bases, sino que también terminaría con un error de recursión. De hecho, el niño establece la base, que sería modificada para llamarse a sí misma con Base.value.fset en una Base.value.fset sin fin.


TL; DR - return self permite que las clases de niños cambien el comportamiento de sus padres. Ver MCVE de la falla a continuación.

Cuando crea la propiedad x en una clase principal, esa clase tiene un atributo x con un setter, getter y eliminador en particular. La primera vez que dices @Parent.x.getter o similar en una clase para niños, estás invocando un método en el miembro x del padre . Si x.getter no copió la instancia de property , al llamarlo desde la clase secundaria cambiaría el getter del padre . Eso evitaría que la clase principal funcione de la manera en que fue diseñada. (Gracias a Martijn Pieters (no es sorpresa) here .)

Y además, los docs requieren:

Un objeto de propiedad tiene métodos getter, setter y deleter utilizables como decoradores que crean una copia de la propiedad ...

Un ejemplo, que muestra las partes internas:

class P: ## @property --- inner workings shown below, marked "##" def x(self): return self.__x x = property(x) ## what @property does ## @x.setter def some_internal_name(self, x): self.__x = x x = x.setter(some_internal_name) ## what @x.setter does class C(P): ## @P.x.getter # x is defined in parent P, so you have to specify P.x def another_internal_name(self): return 42 # Remember, P.x is defined in the parent. # If P.x.getter changes self, the parent''s P.x changes. x = P.x.getter(another_internal_name) ## what @P.x.getter does # Now an x exists in the child as well as in the parent.

Si getter mutara y regresara a self como sugirió, la x del niño sería exactamente la x del padre, y ambas habrían sido modificadas.

Sin embargo, dado que la especificación requiere que getter devuelva una copia, la x del niño es una copia nueva con another_internal_name como fget , y la x del padre no está intacta.

MCVE

Es un poco largo, pero muestra el comportamiento en Py 2.7.14.

class OopsProperty(object): "Shows what happens if getter()/setter()/deleter() don''t copy" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError("unreadable attribute") return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError("can''t set attribute") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError("can''t delete attribute") self.fdel(obj) ########## getter/setter/deleter modified as the OP suggested def getter(self, fget): self.fget = fget return self def setter(self, fset): self.fset = fset return self def deleter(self, fdel): self.fdel = fdel return self class OopsParent(object): # Uses OopsProperty() instead of property() def __init__(self): self.__x = 0 @OopsProperty def x(self): print("OopsParent.x getter") return self.__x @x.setter def x(self, x): print("OopsParent.x setter") self.__x = x class OopsChild(OopsParent): @OopsParent.x.getter # changes OopsParent.x! def x(self): print("OopsChild.x getter") return 42; parent = OopsParent() print("OopsParent x is",parent.x); child = OopsChild() print("OopsChild x is",child.x); class Parent(object): # Same thing, but using property() def __init__(self): self.__x = 0 @property def x(self): print("Parent.x getter") return self.__x @x.setter def x(self, x): print("Parent.x setter") self.__x = x class Child(Parent): @Parent.x.getter def x(self): print("Child.x getter") return 42; parent = Parent() print("Parent x is",parent.x); child = Child() print("Child x is",child.x);

Y la carrera:

$ python foo.py OopsChild.x getter <-- Oops! parent.x called the child''s getter (''OopsParent x is'', 42) <-- Oops! OopsChild.x getter (''OopsChild x is'', 42) Parent.x getter <-- Using property(), it''s OK (''Parent x is'', 0) <-- What we expected from the parent class Child.x getter (''Child x is'', 42)