让我们从一点历史开始,因为最初的实现已经等同于您的替代方案(等同于因为property 是在 CPython 中用 C 实现的,所以 getter 等是用 C 而不是“普通 Python”编写的)。
然而,早在 2007 年它就被报道为 issue (1620) on the Python bug tracker:
正如 Duncan Booth 在
http://permalink.gmane.org/gmane.comp.python.general/551183新
@spam.getter 语法修改了适当的属性,但它应该创建
一个新的。
补丁是修复的初稿。我必须编写单元测试
验证补丁。它复制了财产,并作为奖励抓住了
__doc__ 来自 getter 的字符串,如果文档字符串最初来自
吸气剂也是如此。
不幸的是,链接没有去任何地方(我真的不知道为什么它被称为“永久链接”......)。它被归类为错误并更改为当前形式(请参阅this patch 或相应的Github commit (but it's a combination of several patches))。如果您不想点击链接,更改是:
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__)
为什么要改?
由于链接失效我不知道具体原因,但是我可以根据添加的test-cases in that commit推测:
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")
这与其他答案已经提供的示例类似。问题是您希望能够在不影响父类的情况下更改子类中的行为:
>>> b = BaseClass()
>>> b.spam
5
但是对于您的财产,它会导致:
>>> b = BaseClass()
>>> b.spam
---------------------------------------------------------------------------
PropertyGet Traceback (most recent call last)
PropertyGet: 5
这是因为BaseClass.spam.getter(在SubClass 中使用)实际上修改并返回了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”的方法。
- 已因错误报告而更改。
- 与覆盖父类属性的子类一起使用时,它的行为“有问题”。
- 更一般地说:装饰器无法影响它们将绑定的名称,因此在装饰器中它始终对
return self 有效的假设可能是有问题的(对于通用装饰器)。