Search code examples
pythonpython-2.7classnew-style-class

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


Using python 2.7, suppose I have a Test class with the new-style class syntax defined below.

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

Run the code above will print 5,4,4 which is the desired behavior. However, if I change the first line of the code above to class Test: then the results becomes 5,4,5.

Does anyone know what causes this difference in output?


Solution

  • 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}