让我们从一点历史开始,因为最初的实现与您的替代方案相当(相当于
property
在CPython中是用C实现的,所以
getter
等是用C而不是“纯Python”编写的)。
但是这已经在2007年报告给
Python错误跟踪器上的问题(1620)了:
正如Duncan Booth在http://permalink.gmane.org/gmane.comp.python.general/551183中所述,新的@spam.getter语法会直接修改属性,但应该创建一个新的属性。
此补丁是修复的第一份草案。我必须编写单元测试来验证该补丁。它复制属性,并作为奖励,如果最初的文档字符串也来自getter,则从getter中获取__doc__
字符串。
不幸的是,这个链接没有任何作用(我真的不知道为什么它被称为“永久链接”...)。它被归类为错误,并更改为当前形式(请参见此补丁或相应的Github提交(但它是几个补丁的组合))。如果您不想跟随链接,更改内容如下:
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);
}
同样适用于 setter
和 deleter
。如果您不了解 C 语言,则重要的代码行是:
((propertyobject *)self)->prop_get = getter;
并且
return self;
其余部分大多是“Python C API样板代码”。然而,这两行代码等效于:
self.fget = fget
return self
然后它被更改为:
return property_copy(self, getter, NULL, NULL, NULL);
这基本上做到了:
return type(self)(fget, self.fset, self.fdel, self.__doc__)
为什么它被改变了?
由于链接失效,我不知道确切的原因,但是基于在该提交中添加的测试用例,我可以推测:
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):
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):
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")
这与其他答案已经提供的示例类似。问题在于您希望能够在子类中更改行为而不影响父类:
>>> b = BaseClass()
>>> b.spam
5
然而,对于您的属性,它会导致以下结果:
>>> b = BaseClass()
>>> b.spam
---------------------------------------------------------------------------
PropertyGet Traceback (most recent call last)
PropertyGet: 5
这是因为在SubClass中使用的BaseClass.spam.getter(实际上修改并返回了BaseClass.spam属性!)
所以是的,它已经被改变了(很可能),因为它允许在子类中修改属性的行为而不改变父类的行为。
另一个原因(?)
请注意,还有一个额外的原因,有点傻,但实际上值得一提(我个人认为):
让我们简要回顾一下:装饰器只是赋值的语法糖,所以:
@decorator
def decoratee():
pass
等同于:
def func():
pass
decoratee = decorator(func)
del func
重要的一点是装饰器的结果被赋值给被装饰函数的名称。因此,虽然通常情况下你会为getter/setter/deleter使用相同的“函数名”,但其实不必如此!
例如:
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
在这个例子中,您使用
a
的描述符来创建另一个描述符
b
,它的行为类似于
a
,只是它有一个
setter
。这是一个相当奇怪的例子,可能很少使用(或根本不使用)。但即使它相当奇怪并且(对我来说)风格不太好,它也应该说明仅仅因为您使用了
property_name.setter
(或
getter
/
deleter
),它就必须绑定到
property_name
。它可以绑定到任何名称!我不希望它回传到原始属性(虽然我不确定我在这里期望什么)。
总结
- CPython实际上曾经在
getter
,setter
和deleter
中使用“修改并返回self
”方法。
- 由于错误报告而被更改。
- 当与覆盖父类属性的子类一起使用时,它会表现出“buggy”的行为。
- 更一般地说:装饰器不能影响它们将绑定到哪个名称,因此在装饰器中假设在
return self
可能是有问题的(对于通用装饰器而言)。