Can you easily create a list-like object in python that uses something like a descriptor for its items?

StackOverflow https://stackoverflow.com/questions/8839305

  •  27-10-2019
  •  | 
  •  

Question

I'm trying to write an interface that abstracts another interface somewhat.

The bottom interface is somewhat inconsistent about what it requires: sometimes id's, and sometimes names. I'm trying to hide details like these.

I want to create a list-like object that will allow you to add names to it, but internally store id's associated with those names.

Preferably, I'd like to use something like descriptors for class attributes, except that they work on list items instead. That is, a function (like __get__) is called for everything added to the list to convert it to the id's I want to store internally, and another function (like __set__) to return objects (that provide convenience methods) instead of the actual id's when trying to retrieve items from the list.

So that I can do something like this:

def get_thing_id_from_name(name):
    # assume that this is more complicated
    return other_api.get_id_from_name_or_whatever(name)

class Thing(object)
    def __init__(self, thing_id):
        self.id = thing_id
        self.name = other_api.get_name_somehow(id)

    def __eq__(self, other):
        if isinstance(other, basestring):
            return self.name == other

        if isinstance(other, Thing):
            return self.thing_id == other.thing_id

        return NotImplemented

tl = ThingList()

tl.append('thing_one')
tl.append('thing_two')
tl[1] = 'thing_three'

print tl[0].id
print tl[0] == 'thing_one'
print tl[1] == Thing(3)

The documentation recommends defining 17 methods (not including a constructor) for an object that acts like a mutable sequence. I don't think subclassing list is going to help me out at all. It feels like I ought to be able to achieve this just defining a getter and setter somewhere.

UserList is apparently depreciated (although is in python3? I'm using 2.7 though).

Is there a way to achieve this, or something similar, without having to redefine so much functionality?

Was it helpful?

Solution

Yo don't need to override all the list methods -- __setitem__, __init__ and \append should be enough - you may want to have insert and some others as well. You could write __setitem__ and __getitem__ to call __set__ and __get__ methods on a sepecial "Thing" class exactly as descriptors do.

Here is a short example - maybe something like what you want:

class Thing(object):
    def __init__(self, thing):
        self.value = thing
        self.name = str(thing)
    id = property(lambda s: id(s))
    #...
    def __repr__(self):
        return "I am a %s" %self.name 

class ThingList(list):
    def __init__(self, items):
        for item in items:
            self.append(item)
    def append(self, value):
        list.append(self, Thing(value))
    def __setitem__(self, index, value):
        list.__setitem__(self, index, Thing(value))

Example:

>>> a = ThingList(range(3))
>>> a.append("three")
>>> a
[I am a 0, I am a 1, I am a 2, I am a three]
>>> a[0].id
35242896
>>> 

-- edit --

The O.P. commented: "I was really hoping that there would be a way to have all the functionality from list - addition, extending, slices etc. and only have to redefine the get/set item behaviour."

So mote it be - one really has to override all relevant methods in this way. But if what we want to avoid is just a lot of boiler plate code with a lot of functions doing almost the same, the new, overriden methods, can be generated dynamically - all we need is a decorator to change ordinary objects into Things for all operations that set values:

class Thing(object):
    # Prevents duplicating the wrapping of objects:
    def __new__(cls, thing):
        if isinstance(thing, cls):
            return thing
        return object.__new__(cls, thing)

    def __init__(self, thing):
        self.value = thing
        self.name = str(thing)
    id = property(lambda s: id(s))
    #...
    def __repr__(self):
        return "I am a %s" %self.name 

def converter(func, cardinality=1):
    def new_func(*args):
        # Pick the last item in the argument list, which
        # for all item setter methods on  a list is the one
        # which actually contains the values
        if cardinality == 1:
            args = args[:-1] + (Thing(args[-1]  ),)
        else:
            args = args[:-1] + ([Thing(item) for item in args[-1]],)
        return func(*args)
    new_func.func_name = func.__name__
    return new_func

my_list_dict = {}

for single_setter in ("__setitem__", "append", "insert"):
    my_list_dict[single_setter] = converter(getattr(list, single_setter), cardinality=1)

for many_setter in ("__setslice__", "__add__", "__iadd__", "__init__", "extend"):
    my_list_dict[many_setter] = converter(getattr(list, many_setter), cardinality="many")

MyList = type("MyList", (list,), my_list_dict)

And it works thus:

>>> a = MyList()
>>> a
[]
>>> a.append(5)
>>> a
[I am a 5]
>>> a + [2,3,4]
[I am a 5, I am a 2, I am a 3, I am a 4]
>>> a.extend(range(4))
>>> a
[I am a 5, I am a 0, I am a 1, I am a 2, I am a 3]
>>> a[1:2] =  range(10,12)
>>> a
[I am a 5, I am a 10, I am a 11, I am a 1, I am a 2, I am a 3]
>>> 
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top