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);
 }

setterdeleter 类似。如果你不知道 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 实际上在 gettersetterdeleter 中使用了 "modify and return self" 方法一次。
  • 由于错误报告,它已被更改。
  • 当与覆盖父 class 的 属性 的子 class 一起使用时,它表现得 "buggy"。
  • 更一般地说:装饰器无法影响它们将绑定的名称,因此在装饰器中它对 return self 始终有效的假设可能值得怀疑(对于 general-purpose 装饰器)。