Descriptors are not guaranteed to be invoked for old-style classes. From the 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).
So, what's going on here is that Test.a.__set__
is never being invoked, you are simply adding an a
attribute to 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}
What should really surprise you is why does the T.a.__get__
work here at all?
And the answer to that is that in Python 2.2, old-style classes were reimplemented to use descriptors, and that is an implementation detail that should not be relied on. See this question, and the linked issue.
Bottom line, if you are using descriptors, you should only be using them with new-style classes.
Note, if I do use a new-style class, it works as it should:
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}