Python 属性 描述符设计:为什么复制而不是变异?
Python property descriptor design: why copy rather than mutate?
我正在研究 Python 如何在内部实现 property descriptor。根据文档 property()
是根据描述符协议实现的,为方便起见,在此处复制它:
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__)
我的问题是:为什么最后三个方法没有实现如下:
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 函数?
所以你可以使用继承属性?
只是尝试通过举例来回答:
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
如果它是按照您编写的方式实现的,那么 Base.value.setter
也会设置 double,这是不需要的。我们想要一个全新的setter,而不是修改基础的。
编辑:正如@wim 所指出的,在这种特殊情况下,它不仅会修改基础 setter,而且我们还会以递归错误告终。事实上,子 setter 会调用基类,它会被修改为在无限递归中用 Base.value.fset
调用自身。
TL;DR - return self
允许 child class 改变他们 parent 的行为。请参阅下面的失败 MCVE。
当您在 parent class 中创建 属性 x
时,class 具有具有特定 setter、getter 和删除器。第一次在 child class 中说 @Parent.x.getter
或类似内容时,您正在调用 parent 的 上的方法x
成员。如果 x.getter
没有复制 property
实例,从 child class 调用它会改变 parent的getter。这将阻止 parent class 以其设计的方式运行。 (感谢 Martijn Pieters(不足为奇)here。)
此外,docs 需要它:
A property object has getter, setter, and deleter methods usable as decorators that create a copy of the property ...
一个示例,显示内部结构:
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.
如果 getter
按照您的建议突变并 return 编辑 self
,则 child 的 x
将完全是 parent的 x
,两者都会被修改。
但是,因为规范要求 getter
到 return 一个副本,child 的 x
是一个新副本 another_internal_name
作为 fget
,而 parent 的 x
未受影响。
MCVE
有点长,但显示了 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);
和 运行:
$ 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)
让我们从一些历史开始,因为最初的实现等同于您的替代方案(等价因为 property
在 CPython 中用 C 实现,所以 getter
等写在C 不是 "plain Python").
然而它在 2007 年被报道为 issue (1620) on the Python bug tracker:
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.
不幸的是 link 哪儿也去不了(我真的不知道为什么它被称为 "permalink" ...)。它被 class 确定为错误并更改为当前形式(参见 this patch or the corresponding Github commit (but it's a combination of several patches))。如果您不想遵循 link,则更改为:
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 boilerplate"。然而,这两行相当于你的:
self.fget = fget
return self
并改为:
return property_copy(self, getter, NULL, NULL, NULL);
本质上是这样的:
return type(self)(fget, self.fset, self.fdel, self.__doc__)
为什么要更改?
因为 link 挂了我不知道确切的原因,但是我可以根据添加的 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")
这与其他答案已经提供的示例类似。问题是您希望能够在不影响父 class:
的情况下更改子 class 中的行为
>>> b = BaseClass()
>>> b.spam
5
但是你的 属性 会导致这个:
>>> b = BaseClass()
>>> b.spam
---------------------------------------------------------------------------
PropertyGet Traceback (most recent call last)
PropertyGet: 5
发生这种情况是因为 BaseClass.spam.getter
(在 SubClass
中使用)实际上修改了 returns BaseClass.spam
属性!
所以是的,它已经被改变了(很可能)因为它允许在子class中修改属性的行为而不改变父class的行为.
另一个原因(?)
请注意,还有一个额外的原因,有点傻但实际上值得一提(在我看来):
让我们简单回顾一下:装饰器只是赋值的语法糖,所以:
@decorator
def decoratee():
pass
相当于:
def func():
pass
decoratee = decorator(func)
del func
这里的重点是装饰器的结果被分配给装饰函数的名称。因此,虽然您通常对 getter/setter/deleter 使用相同的 "function name" - 但您不必!
例如:
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
中使用了 "modify and return self
" 方法一次。
- 由于错误报告,它已被更改。
- 当与覆盖父 class 的 属性 的子 class 一起使用时,它表现得 "buggy"。
- 更一般地说:装饰器无法影响它们将绑定的名称,因此在装饰器中它对
return self
始终有效的假设可能值得怀疑(对于 general-purpose 装饰器)。
我正在研究 Python 如何在内部实现 property descriptor。根据文档 property()
是根据描述符协议实现的,为方便起见,在此处复制它:
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__)
我的问题是:为什么最后三个方法没有实现如下:
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 函数?
所以你可以使用继承属性?
只是尝试通过举例来回答:
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
如果它是按照您编写的方式实现的,那么 Base.value.setter
也会设置 double,这是不需要的。我们想要一个全新的setter,而不是修改基础的。
编辑:正如@wim 所指出的,在这种特殊情况下,它不仅会修改基础 setter,而且我们还会以递归错误告终。事实上,子 setter 会调用基类,它会被修改为在无限递归中用 Base.value.fset
调用自身。
TL;DR - return self
允许 child class 改变他们 parent 的行为。请参阅下面的失败 MCVE。
当您在 parent class 中创建 属性 x
时,class 具有具有特定 setter、getter 和删除器。第一次在 child class 中说 @Parent.x.getter
或类似内容时,您正在调用 parent 的 上的方法x
成员。如果 x.getter
没有复制 property
实例,从 child class 调用它会改变 parent的getter。这将阻止 parent class 以其设计的方式运行。 (感谢 Martijn Pieters(不足为奇)here。)
此外,docs 需要它:
A property object has getter, setter, and deleter methods usable as decorators that create a copy of the property ...
一个示例,显示内部结构:
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.
如果 getter
按照您的建议突变并 return 编辑 self
,则 child 的 x
将完全是 parent的 x
,两者都会被修改。
但是,因为规范要求 getter
到 return 一个副本,child 的 x
是一个新副本 another_internal_name
作为 fget
,而 parent 的 x
未受影响。
MCVE
有点长,但显示了 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);
和 运行:
$ 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)
让我们从一些历史开始,因为最初的实现等同于您的替代方案(等价因为 property
在 CPython 中用 C 实现,所以 getter
等写在C 不是 "plain Python").
然而它在 2007 年被报道为 issue (1620) on the Python bug tracker:
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.
不幸的是 link 哪儿也去不了(我真的不知道为什么它被称为 "permalink" ...)。它被 class 确定为错误并更改为当前形式(参见 this patch or the corresponding Github commit (but it's a combination of several patches))。如果您不想遵循 link,则更改为:
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 boilerplate"。然而,这两行相当于你的:
self.fget = fget
return self
并改为:
return property_copy(self, getter, NULL, NULL, NULL);
本质上是这样的:
return type(self)(fget, self.fset, self.fdel, self.__doc__)
为什么要更改?
因为 link 挂了我不知道确切的原因,但是我可以根据添加的 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")
这与其他答案已经提供的示例类似。问题是您希望能够在不影响父 class:
的情况下更改子 class 中的行为>>> b = BaseClass()
>>> b.spam
5
但是你的 属性 会导致这个:
>>> b = BaseClass()
>>> b.spam
---------------------------------------------------------------------------
PropertyGet Traceback (most recent call last)
PropertyGet: 5
发生这种情况是因为 BaseClass.spam.getter
(在 SubClass
中使用)实际上修改了 returns BaseClass.spam
属性!
所以是的,它已经被改变了(很可能)因为它允许在子class中修改属性的行为而不改变父class的行为.
另一个原因(?)
请注意,还有一个额外的原因,有点傻但实际上值得一提(在我看来):
让我们简单回顾一下:装饰器只是赋值的语法糖,所以:
@decorator
def decoratee():
pass
相当于:
def func():
pass
decoratee = decorator(func)
del func
这里的重点是装饰器的结果被分配给装饰函数的名称。因此,虽然您通常对 getter/setter/deleter 使用相同的 "function name" - 但您不必!
例如:
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
中使用了 "modify and returnself
" 方法一次。 - 由于错误报告,它已被更改。
- 当与覆盖父 class 的 属性 的子 class 一起使用时,它表现得 "buggy"。
- 更一般地说:装饰器无法影响它们将绑定的名称,因此在装饰器中它对
return self
始终有效的假设可能值得怀疑(对于 general-purpose 装饰器)。