Search code examples
pythonpython-2.7descriptormagic-methods

getting pythons __set__ working


I just wanted to use the descriptor pattern, but it didn't seem to work that well. Here is a short example (without any real use, just to show):

class Num(object):
  def__init__(self, val=0):
    self.val = val
  def __get__(self, instance, owner):
    return self.val
  def __set__(self, instance, val):
    self.val = val
  def __str__(self):
    return "Num(%s)" % self.val
  def __repr__(self):
    return self.__str__()

class Test(object):
  def __init__(self, num=Num()):
    self.num = num

and the Test:

>>>t = Test()
>>>t.num # OK
Num(0)
>>>t.num + 3 #OK i know how to fix that, but I thought __get__.(t.num, t, Test) will be called
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'Num' and 'int'
>>> t.num = 4 # why isn't __set__(t.num, t, 4) called here?
>>> t.num
4

What is my misconception here?


Solution

  • Descriptors only work when they are attributes of a class, not an instance. If you change your class to:

    class Test(object):
        num = Num()
    

    . . . then the descriptor will work.

    However, because the descriptor has to be set on the class, that means there is only one instance of the descriptor, so it's probably not a good idea for the descriptor to store its values on self. Such values will be shared across all instances of the class. Instead, set the value on instance.

    Also, note that your __str__ and __repr__ will probably not do what you think they will. Calling t.num will activate the descriptor and return its val, so the result of t.num will be the plain number 0, not a Num instance. The whole point of the descriptor is to transparently return the result of __get__ without making the descriptor object itself visible.

    Here are some illustrative examples:

    >>> t1 = Test()
    >>> t2 = Test()
    >>> t1.num
    0
    >>> Test.num
    0
    # Accessing the descriptor object itself
    >>> Test.__dict__['num']
    Num(0)
    >>> t1.num = 10
    >>> t1.num
    10
    # setting the value changed it everywhere
    >>> t2.num
    10
    >>> Test.num
    10
    

    With an alternate version of the descriptor:

    class Num(object):
      def __init__(self, val=0):
        self.val = val
    
      def __get__(self, instance, owner):
        try:
            return instance._hidden_val
        except AttributeError:
            # use self.val as default
            return self.val
    
      def __set__(self, instance, val):
        instance._hidden_val = val
    
    class Test(object):
        num = Num()
    
    >>> t1 = Test()
    >>> t2 = Test()
    >>> t1.num
    0
    >>> t1.num = 10
    >>> t1.num
    10
    # Now there is a separate value per instance
    >>> t2.num
    0