Python属性描述符设计:为什么复制而不是改变?

27

我正在研究Python内部如何实现属性描述符。根据文档,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函数?

3个回答

14
让我们从一点历史开始,因为最初的实现与您的替代方案相当(相当于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);
 }

同样适用于 setterdeleter。如果您不了解 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):
        # 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

这是因为在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实际上曾经在gettersetterdeleter中使用“修改并返回self”方法。
  • 由于错误报告而被更改。
  • 当与覆盖父类属性的子类一起使用时,它会表现出“buggy”的行为。
  • 更一般地说:装饰器不能影响它们将绑定到哪个名称,因此在装饰器中假设在return self可能是有问题的(对于通用装饰器而言)。

2
+460 用于查找历史记录和测试用例。 - wim

8
TL;DR - return self 允许子类更改其父类的行为。以下是 MCVE 的失败示例。
当你在父类中创建属性 x 时,该类具有带有特定设置器、获取器和删除器的属性 x。第一次在子类中使用类似 @Parent.x.getter 的语句时,你正在调用 父类的 x 成员的方法。如果 x.getter 没有复制 property 实例,则从 子类 调用它会更改 父类的 getter。这将阻止父类按照设计运作。(感谢 Martijn Pieters(不出所料)在此处。)
此外,文档也要求:

一个 property 对象具有可用作修饰符的 getter、setter 和 deleter 方法,用于创建属性的副本……

下面是一个例子,展示了内部实现:
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改变并返回self,那么子项的x将完全等于父项的x,并且两者都将被修改。然而,由于规范要求getter返回一个副本,子项的x是带有另一个内部名称作为fget的新副本,而父项的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)

7

所以你可以在继承中使用属性吗?

这是一个尝试通过示例回答的问题:

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也会设置双倍值,这是不期望的。我们希望有一个全新的setter,而不是修改基础setter。
编辑:正如@wim指出的那样,在这种特殊情况下,它不仅会修改基本setter,而且我们还会遇到递归错误。实际上,子setter将调用基本setter,该基本setter将被修改为使用Base.value.fset无限递归地调用自身。

1
关闭。我认为子设置器会导致递归堆栈溢出。 - wim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接