关于继承:python属性描述符设计:为什么复制而不是变异?

Python property descriptor design: why copy rather than mutate?

我在研究Python如何在内部实现属性描述符。根据文件,property()是根据描述符协议实现的,为了方便起见,在这里复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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__)

我的问题是:为什么最后三个方法没有实现如下:

1
2
3
4
5
6
7
8
9
10
11
    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函数?


让我们从一些历史开始,因为最初的实现等价于您的替代方案(相当于因为property在Cpython中的C中实现,所以getter等是用C而不是"纯Python"编写的)。

然而,2007年在python bug tracker上被报告为问题(1620):

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.

不幸的是,这个链接不在任何地方(我真的不知道为什么它被称为"permalink"…)。它被归类为bug并更改为当前形式(请参阅此补丁或相应的github提交(但它是多个补丁的组合))。如果您不想跟踪链接,则更改为:

1
2
3
4
5
6
7
8
9
10
11
12
 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,重要的是:

1
((propertyobject *)self)->prop_get = getter;

1
return self;

其余大部分是"python c api样板"。然而,这两行相当于:

1
2
self.fget = fget
return self

改为:

1
return property_copy(self, getter, NULL, NULL, NULL);

基本上是这样的:

1
return type(self)(fget, self.fset, self.fdel, self.__doc__)

为什么改变了?

由于链接断开,我不知道确切的原因,但是我可以根据提交中添加的测试用例进行推测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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")

这类似于其他答案已经提供的例子。问题是,您希望能够在不影响父类的情况下更改子类中的行为:

1
2
3
>>> b = BaseClass()
>>> b.spam
5

但是,对于您的财产,这将导致:

1
2
3
4
5
>>> b = BaseClass()
>>> b.spam
---------------------------------------------------------------------------
PropertyGet                               Traceback (most recent call last)
PropertyGet: 5

这是因为BaseClass.spam.getter(在SubClass中使用)实际修改并返回BaseClass.spam属性!

所以是的,它已经被改变了(很可能),因为它允许在不改变父类行为的情况下修改子类中属性的行为。

另一个原因(?)

请注意,还有一个额外的原因,这有点愚蠢,但实际上值得一提(在我看来):

让我们简短地回顾一下:装饰师只是一个任务的语法甜头,所以:

1
2
3
@decorator
def decoratee():
    pass

相当于:

1
2
3
4
5
def func():
    pass

decoratee = decorator(func)
del func

这里重要的一点是,将decorator的结果分配给decorated函数的名称。因此,虽然您通常对getter/setter/deleter使用相同的"函数名",但您不必这样做!

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

在本例中,您使用EDOCX1的描述符(7)为b创建另一个描述符,该描述符的行为类似于a,但它得到了setter

这是一个相当奇怪的例子,可能不会经常使用(或者根本不用)。但是,即使它很奇怪,而且(对我来说)不是很好的风格,它也应该说明,仅仅因为你使用了property_name.setter(或getter/deleter,它必须绑定到property_name。它可以绑定到任何名称!我不希望它传播回原始属性(尽管我不确定在这里会发生什么)。

总结

  • cpython在gettersetterdeleter中实际使用了一次"修改并返回self方法。
  • 因为一个错误报告,它被更改了。
  • 当与覆盖父类属性的子类一起使用时,它的行为是"错误的"。
  • 更一般地说:装饰师不能影响他们将要绑定的名称,所以假设它对装饰师中的return self始终有效可能是可疑的(对于通用装饰师)。


医生允许儿童班改变父母的行为。见下面故障的MCVE。

在父类中创建属性x时,该类具有具有具有特定setter、getter和deleter的属性x。当您在子类中第一次说@Parent.x.getter或类似的话,您将在父类的x成员上调用一个方法。如果x.getter没有复制property实例,从子类调用该实例将改变父类的getter。这将阻止父类按照其设计的方式进行操作。(多亏了马蒂金·彼得斯(毫不奇怪)。

此外,文件要求:

A property object has getter, setter, and deleter methods usable as decorators that create a copy of the property ...

一个示例,显示了内部结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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是一份新副本,another_internal_namefget的版本,而家长的x是未经修改的。

麦克维尔

它有点长,但在PY2.7.14上显示了它的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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);

和运行:

1
2
3
4
5
6
7
8
9
$ 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)


所以可以使用继承属性?

只是举个例子来回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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,这是不需要的。我们需要一个全新的设置器,而不是修改基础设置器。

edit:正如@wim所指出的,在这种特殊情况下,它不仅会修改基setter,还会导致递归错误。实际上,子设置器会调用基设置器,它将被修改为在无休止的递归中使用Base.value.fset调用自己。