Search code examples
pythonclassgetter-setter

How to add and bind descriptors dynamically in python?


I would like to dynamically bind descriptors to attribute of a class.

For example I have this descriptor (it is just a dummy example):

class Item(object):
    def __init__(self, filename):
        self.filename = filename

    def __get__(self, obj=None, objtype=None):
        #print '__get__(%s, %s)' % (obj, objtype)
        return open(self.filename).read()

    def __set__(self, obj, val):
        #print '__set__(%s, %s)' % (obj, val)
        open(self.filename, 'w').write(str(val))

In my main container, I would like to dynamically register my descriptors.

Everything works great if I instanciate the descriptors at the class level:

class Container(object):
    foo = Item('foo')
    bar = Item('bar')

Unfortunately when I try to associate the descriptor dynamically using setattr I need to put a lot more complexity to my class:

class Container(object):
    def __init__(self, data):
        for attr in data:
            super(Container, self).__setattr__(attr, Item(attr))

    def __setattr__(self, name, value):   
        #print '__setattr__(%s, %s)' % (name, value)
        attr = super(Container, self).__getattribute__(name)
        if hasattr(attr, '__set__'):
            attr.__set__(name, value)
        else:
            super(Container, self).__setattr__(name, value)        

    def __getattribute__(self, name):
        #print '__getattribute__(%s)' % (name)
        attr = super(Container, self).__getattribute__(name)
        if hasattr(attr, '__get__'):
            return attr.__get__(name)
        return attr

The expected output is:

>>> c = Container(['foo', 'bar'])  
>>> c.foo = 2
>>> c.foo
'2'

Is there a simpler solution with less kludges?


Solution

  • So, you're almost there with your __init__ in container. The problems you have:

    1. in 99.9% cases you sould never call magic (dunder, __) functions directly. So, your super(...).__setattr__ makes no sense, tbh. There's setattr for this
    2. Tricky part with descriptors (btw, from my experience, it's kind of "default" obstruction when people start using them). When you use descriptors in non-dynamic way with

      class Container(object):
          foo = Item('foo')
          bar = Item('bar')
      

      you are setting foo and bar in scope of the class - literally as class attributes. But in your "dynamic" way you're doing it with instance. Idk if you tried to set it as class, but if this was an intention, super doesn't work like this. To set attach descriptor dynamically, you need to attach it to class of your instance (referred by self inside __init__). To do so, access self.__class__ or type(self). So, your code may look like

      class Container(object):
          def __init__(self, data):
              for attr in data:
                  setattr(type(self), attr, Item(attr))