getting pythons __set__ working

77 views Asked by At

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?

2

There are 2 answers

1
BrenBarn On BEST ANSWER

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
0
justengel On

As BrenBarn stated Descriptors appear to be for Class variables. You might be interested in looking at Pythons properties.

class GenericItem(object):
    """Generic item descriptor"""

    def __init__(self, value=None, name=""):
        super().__init__()

        self.value = value
        self.name = name
    # end Constructor

    def __get__(self, obj, objtype):
#         print(self, obj, objtype)
        return self.value
    # end __get__

    def __set__(self, obj, value):
#         print(self, obj, value)
        self.value = value
    # end __set__

    def __str__(self):
        if self.name is None or self.name == "":
            return str(self.value)
        return self.name +"("+str(self.value)+")"
    # end __str__
# end class Num

class Test(object):
    def __init__(self, value=0):
        super().__init__()

        self._num = GenericItem(value, "Number")
    # end Constructor

    @property
    def num(self):
        """This is a number"""
        return self._num
    @num.setter
    def num(self, value):
        self._num.__set__(None, value)
    # end num property
# end class Test

if __name__ == "__main__":
    g = GenericItem(1, "Generic")
    print(g)
    g = 5
    print(g)


    t = Test()
    print(t.num)
    try:
        t.num + 3 # We didn't implement any sort of addition __add__
    except:
        pass
    t.num = 4
    print(t.num)

Result:

Generic(1)
5
Number(0)
Number(4)

Properties help control how instance variables are set.