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 elgetter
,setter
ydeleter
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)