Python property descriptor design: why copy rather than mutate?
我在研究Python如何在内部实现属性描述符。根据文件,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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__) |
我的问题是:为什么最后三个方法没有实现如下:
1 2 3 4 5 6 7 8 9 10 11 | 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 |
是否有理由重新生成属性的新实例,内部指向基本相同的get和set函数?
让我们从一些历史开始,因为最初的实现等价于您的替代方案(相当于因为
然而,2007年在python bug tracker上被报告为问题(1620):
As reported by Duncan Booth at
http://permalink.gmane.org/gmane.comp.python.general/551183 the new
@spam.getter syntax modifies the property in place but it should create
a new one.The patch is the first draft of a fix. I've to write unit tests to
verify the patch. It copies the property and as a bonus grabs the
__doc__ string from the getter if the doc string initially came from the
getter as well.
不幸的是,这个链接不在任何地方(我真的不知道为什么它被称为"permalink"…)。它被归类为bug并更改为当前形式(请参阅此补丁或相应的github提交(但它是多个补丁的组合))。如果您不想跟踪链接,则更改为:
1 2 3 4 5 6 7 8 9 10 11 12 | 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); } |
与
1 | ((propertyobject *)self)->prop_get = getter; |
和
1 | return self; |
其余大部分是"python c api样板"。然而,这两行相当于:
1 2 | self.fget = fget return self |
改为:
1 | return property_copy(self, getter, NULL, NULL, NULL); |
基本上是这样的:
1 | return type(self)(fget, self.fset, self.fdel, self.__doc__) |
为什么改变了?
由于链接断开,我不知道确切的原因,但是我可以根据提交中添加的测试用例进行推测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | 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") |
这类似于其他答案已经提供的例子。问题是,您希望能够在不影响父类的情况下更改子类中的行为:
1 2 3 | >>> b = BaseClass() >>> b.spam 5 |
但是,对于您的财产,这将导致:
1 2 3 4 5 | >>> b = BaseClass() >>> b.spam --------------------------------------------------------------------------- PropertyGet Traceback (most recent call last) PropertyGet: 5 |
这是因为
所以是的,它已经被改变了(很可能),因为它允许在不改变父类行为的情况下修改子类中属性的行为。
另一个原因(?)请注意,还有一个额外的原因,这有点愚蠢,但实际上值得一提(在我看来):
让我们简短地回顾一下:装饰师只是一个任务的语法甜头,所以:
1 2 3 | @decorator def decoratee(): pass |
相当于:
1 2 3 4 5 | def func(): pass decoratee = decorator(func) del func |
这里重要的一点是,将decorator的结果分配给decorated函数的名称。因此,虽然您通常对getter/setter/deleter使用相同的"函数名",但您不必这样做!
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 |
在本例中,您使用EDOCX1的描述符(7)为
这是一个相当奇怪的例子,可能不会经常使用(或者根本不用)。但是,即使它很奇怪,而且(对我来说)不是很好的风格,它也应该说明,仅仅因为你使用了
- cpython在
getter 、setter 和deleter 中实际使用了一次"修改并返回self 方法。 - 因为一个错误报告,它被更改了。
- 当与覆盖父类属性的子类一起使用时,它的行为是"错误的"。
- 更一般地说:装饰师不能影响他们将要绑定的名称,所以假设它对装饰师中的
return self 始终有效可能是可疑的(对于通用装饰师)。
医生允许儿童班改变父母的行为。见下面故障的MCVE。
在父类中创建属性
此外,文件要求:
A property object has getter, setter, and deleter methods usable as decorators that create a copy of the property ...
一个示例,显示了内部结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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. |
如果
但是,由于规范要求
它有点长,但在PY2.7.14上显示了它的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | 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); |
和运行:
1 2 3 4 5 6 7 8 9 | $ 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) |
所以可以使用继承属性?
只是举个例子来回答:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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 |
如果它是以您编写它的方式实现的,那么
edit:正如@wim所指出的,在这种特殊情况下,它不仅会修改基setter,还会导致递归错误。实际上,子设置器会调用基设置器,它将被修改为在无休止的递归中使用