Python: A List of Descriptors
Asked Answered
R

2

6

I am trying to use use a list to reference a sequence of descriptors without success. There are a list of objects (_b) defined by an external library (class A) that I would like to access via descriptors (class Descriptor). In the example below, b is assigned a list of references to the descriptor, but when any item of the list is assigned a value, the reference to the descriptor is overwritten by the value instead of passing the value to the descriptor. I am apparently missing a fundamental behavior of descriptors even after reading several references and articles on descriptors.

class Descriptor(object):
    def __init__(self, varname):
        self.varname = varname
        pass

    def __get__(self, instance, owner):
        print('get', self.varname)
        #return getattr(getattr(instance, self.varname),"get")()
        return instance.__dict__[self.varname].get()

    def __set__(self, instance, value):
        print('set', self.varname)
        #getattr(getattr(instance, self.varname),"set")(value)
        instance.__dict__[self.varname].set(value)

class A(object):
    def __init__(self, value=None):
        self.value = value
    def get(self):
        return self.value
    def set(self, value):
        self.value = value

class C(object):
    print "root"
    a = Descriptor('_a')
    b = [Descriptor('_b[x]') for x in range(5)]
    def __init__(self, val):
        print "init"
        self._a = A()
        self.a = val
        self._b = [A() for x in range(5)]
        self.b[0] = 1
c = C(3)
d = C(4)

print c._a.get()
print c.a
print d._a.get()
print d.a
print c.b[0]

In my actual program the external library is a gui library which I would like to abstract so different interfaces can easily be exchanged. Several views in the gui contain columns of entry boxes (up to 40 per column) that correspond to lists in the program.

Also, which is the preferred method of accessing member function of the instance object passed into the descriptor: getattr or __dict__. __dict__ seems cleaner but I didn't know if there are any framework or usability issues in using it.

Any help on the question asked or suggestions of other approaches to meet my needs in the program are appreciated. Thanks.

Per millimoose's recommendation, the following list-like class seems to meet my needs. Any pitfalls with this method other than the descriptor being defined in the class root while the 'list descriptor' in the class __init__ as well as having to provide the class as an argument when initializing? Other list functions need to be added and special index behaviors such as negative indexes needs to be added.

class DescriptorList(object):
    def __init__(self, owner, varname):
        self.owner = owner
        self.varname = varname

    def __getitem__(self, index):
        print('getitem', self.varname, index)
        return getattr(getattr(self.owner, self.varname)[index],"get")()

    def __setitem__(self, index, value):
        print('setitem', self.varname, index)
        getattr(getattr(self.owner, self.varname)[index],"set")(value)

class C(object):
    a = Descriptor('_a')
    def __init__(self, val):
        self._a = A()
        self.a = val
        self._b = [A() for x in range(5)]
        self.b = DescriptorList(self, '_b')
        for i in range(5):
            self.b[i] = i

c = C(3)
print [c.b[i] for i in range(5)]

Also, with DescriptorList being instanced in C.__init__, the code could be simplified so that the DescriptorList uses the object itself instead of the object's name. Are there any advantages or disadvantages to this method?

class DescriptorList(object):
    def __init__(self, var):
        self.var = var

    def __getitem__(self, index):
        print('get', self.var, index)
        return self.var[index].get()

    def __setitem__(self, index, value):
        print('set', self.var, index)
        self.var[index].set(value)

class C(object):
    a = Descriptor('_a')
    def __init__(self, val):
        self._a = A()
        self.a = val
        self._b = [A() for x in range(5)]
        self.b = DescriptorList(self._b)
        for i in range(5):
            self.b[i] = i

Why are __get__ and __set__ handled differently from __getitem__ and __setitem__?

Resnick answered 19/11, 2012 at 2:50 Comment(8)
Accessing __dict__ will fail for classes that use __slots__, or namedtuples, and who knows what else. Basically, an arbitrary Python object is not guaranteed to have a __dict__.Adaptive
I'm having a hard time figuring out what you expect: b = [Descriptor('_b[x]') for x in range(5)] to do. descriptors only work when bound directly to a class, not put in a list which is bound to the class. Even if that did work, you don't usually name a variable _b[x]Galleywest
Also, for b, you don't want a descriptor. You need a list-like object that delegates to the appropriate methods of A instances, or creates them, etc.Adaptive
@Galleywest I think he wants c.b[0] = 123 to ultimately call the method A.set() corresponding to a wrapper object for b[0]. (Which of course can't be done with descriptors.)Adaptive
@Adaptive Thanks for the info on __dict__. Are there any disadvantages or considerations using getattr?Resnick
@Adaptive Yes, I was looking for something along the lines of c.b[0]=123 ultimately calling A.set(). If this cannot be done with descriptors, is there another 'descriptor' like method that can be used or should I just access the appropriate member functions directly, e.g. A.set(), maybe creating a wrapper class to handle differences in the external libraries?Resnick
@Resnick I'm not aware of a class that would do this directly. What you want is a "fake" list that would automatically wrap / unwrap values in A. The best way to do this would be to implement the MutableSequence ABC. As for getattr, I think it should be safe to use that one. It'd probably be a bug in Python if it couldn't retrieve an attribute that you can access directly.Adaptive
I added a couple of possible approaches based on millimoose's comments that I think will meet my needs. For consistency it would be nice if classes implementing __get__ and __getitem__ were handled similarly.Resnick
R
0

Based on feedback from millimoose and another post that is similar in nature, the following code appears to meet my needs though it seems a bit of a kludge.

class Descriptor(object):
    def __init__(self, varname, index=None):
        self.varname = varname
        self.index = index

    def __get__(self, instance, owner):
        print('get', self.varname, self.index)
        if self.index is not None:
            return getattr(getattr(instance, self.varname)[self.index],"get")()
        else:
            return getattr(getattr(instance, self.varname),"get")()

    def __set__(self, instance, value):
        print('set', self.varname, self.index)
        if self.index is not None:
            getattr(getattr(instance, self.varname)[self.index],"set")(value)
        else:
            getattr(getattr(instance, self.varname),"set")(value)

class DescriptorList(list):
    def __init__(self, initlist, instance):
        self.instance = instance
        super(DescriptorList,self).__init__(initlist)

    def __getitem__(self, index):
        print('getitem', self.instance, index)
        return super(DescriptorList,self).__getitem__(index).__get__(self.instance, self.instance.__class__)

    def __setitem__(self, index, value):
        print('setitem', self.instance, index, value)
        super(DescriptorList,self).__getitem__(index).__set__(self.instance, value)

class A(object):
    def __init__(self, value=None):
        self.value = value
    def get(self):
        return self.value
    def set(self, value):
        self.value = value

class C(object):
    a = Descriptor('_a')
    b = [Descriptor('_b', x) for x in range(5)]

    def __init__(self, val):
        self._a = A()
        self.a = val
        self._b = [A() for x in range(5)]
        self.b = DescriptorList(self.__class__.b, self)
        for i in range(5):
            self.b[i] = i*val

c = C(3)
d = C(4)
print c.a
print d.a
print [c.b[i] for i in range(5)]
print [d.b[i] for i in range(5)]

Any comments on limitations or errors with this solution or suggested improvements are appreciated.

Resnick answered 19/11, 2012 at 21:12 Comment(0)
S
0

I needed something similar and I came up with the following:

class Element(object):
  def __get__(self, obj, objtype=None):
    print("Element.__get__(), obj.idx={0}".format(obj.idx))
    return random.randint(1, 10)

  def __set__(self, obj, value):
    print("Element.__set__(), obj.idx={0}, value={1}".format(obj.idx, value))

class Indexable(object):
  # Can also sub-class from list; I needed mine to be fixed length 
  # because I was mapping them to a set of hardware registers.

  element = Element()

  def __init__(self, sz):
    self.sz = sz
    self.idx = 0

  def __getitem__(self, idx):
    self.idx = idx
    return self.element

  def __setitem__(self, idx, value):
    self.idx = idx
    self.element = element

class Owner(object):
  def __init__(self):
    self.seq = Indexable(5)

The Element.__get__ calls random.randint() but obviously, it's arbitrary. What I needed was the index so the get/set functions know which register to read/write.

Spokeswoman answered 3/10, 2013 at 17:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.