为什么 setter 在新式 class 和旧式 class 中表现不同

Why does setter behave differently in new-style class and old-style class

使用 python 2.7,假设我有一个 Test class 以及下面定义的新式 class 语法。

class Test(object):
  def __init__(self):
    self._a = 5

  @property
  def a(self):
    return self._a

  @a.setter
  def a(self, val):
    self._a = val

t = Test()
print t.a
t.a = 4
print t.a
print t._a

运行 上面的代码将打印 5,4,4,这是所需的行为。 但是,如果我将上面代码的第一行更改为 class Test:,那么结果将变为 5,4,5

有谁知道造成这种输出差异的原因吗?

描述符不能保证为旧式 classes 调用。来自 docs:

The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x has a lookup chain starting with a.__dict__['x'], then type(a).__dict__['x'], and continuing through the base classes of type(a) excluding metaclasses. If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined. Note that descriptors are only invoked for new style objects or classes (a class is new style if it inherits from object or type).

所以,这里发生的事情是 Test.a.__set__ 永远不会被调用,您只是将 a 属性添加到 t:

In [8]: class Test:
    ...:   def __init__(self):
    ...:     self._a = 5
    ...:
    ...:   @property
    ...:   def a(self):
    ...:     return self._a
    ...:
    ...:   @a.setter
    ...:   def a(self, val):
    ...:     self._a = val
    ...:

In [9]: t = Test()

In [10]: vars(t)
Out[10]: {'_a': 5}

In [11]: t.a
Out[11]: 5

In [12]: t._a
Out[12]: 5

In [13]: t.a = 100

In [14]: t.a
Out[14]: 100

In [15]: t._a
Out[15]: 5

In [16]: vars(t)
Out[16]: {'_a': 5, 'a': 100}

真正让您感到惊讶的是 为什么 T.a.__get__ 在这里工作

答案是在 Python 2.2 中,旧式 classes 被重新实现以使用描述符,这是一个不应依赖的实现细节。参见 this question, and the linked issue

最重要的是,如果您使用描述符,您应该只将它们与新式 classes 一起使用。

请注意,如果我确实使用新样式 class,它会正常工作:

In [17]: class Test(object):
    ...:   def __init__(self):
    ...:     self._a = 5
    ...:
    ...:   @property
    ...:   def a(self):
    ...:     return self._a
    ...:
    ...:   @a.setter
    ...:   def a(self, val):
    ...:     self._a = val
    ...:

In [18]: t = Test()

In [19]: vars(t)
Out[19]: {'_a': 5}

In [20]: t.a = 100

In [21]: t.a
Out[21]: 100

In [22]: t._a
Out[22]: 100

In [23]: vars(t)
Out[23]: {'_a': 100}