Custom wrapper for indexing python list starting a

2019-05-11 19:00发布

问题:

I'd like to write a simple wrapper for the python list type that forces it to start indexing at 1 instead of 0. I've got a a fairly complex program based on some discrete probability distributions of duration data, with integer-length buckets, but I don't have any durations of less than 1. Anyway, it would greatly simplify a some significant portions of my code to be able to seamlessly index starting at 1. I was using a dict at first but I found several properties of them to be too cumbersome.

I've never written a wrapper for a Python class before, much less a built-in type, but I feel like what I want to do is fairly simple. At the bare minimum, I should be able to do stuff like this:

>>> p = one_list([1,2,3,4,5])
>>> for i in range(1,6):
    print i, p[i]

1 1
2 2
3 3
4 4
5 5
>>> len(p)
5

However it would be good if I could overwrite some of the other relevant built-in methods of the list class as well, such as index.

>>> len(p)
5
>>> p.index(p[-1])
5

Please share your tips as to how I might go about doing something like this. I was considering whether or not to just do it with a custom class, but that seems like it might be overkill. I also welcome any recommendations as to useful methods to overwrite.

Edit: Afterword

I'd just like to note that it was not worth the trouble to do this, and the reason I've accepted the answer below is not because I tried to implement it in the way he describes, but because he helped me realize that lists are good enough on their own.

回答1:

Here's a complete (I think) implementation of a 1-based list, correctly handling slicing (including extended slices), indexing, popping, etc. It's slightly more tricky to get this right than you might think, especially the slicing and the negative indexes. In fact I'm still not 100% sure it works exactly as it should, so caveat coder.

class list1(list):
    """One-based version of list."""

    def _zerobased(self, i):
        if type(i) is slice:
            return slice(self._zerobased(i.start),
                         self._zerobased(i.stop), i.step)
        else:
            if i is None or i < 0:
                return i
            elif not i:
                raise IndexError("element 0 does not exist in 1-based list")
            return i - 1

    def __getitem__(self, i):
        return list.__getitem__(self, self._zerobased(i))

    def __setitem__(self, i, value):
        list.__setitem__(self, self._zerobased(i), value)

    def __delitem__(self, i):
        list.__delitem__(self, self._zerobased(i))

    def __getslice__(self, i, j):
        print i,j
        return list.__getslice__(self, self._zerobased(i or 1),
                                 self._zerobased(j))

    def __setslice__(self, i, j, value):
        list.__setslice__(self, self._zerobased(i or 1),
                          self._zerobased(j), value)

    def index(self, value, start=1, stop=-1):
        return list.index(self, value, self._zerobased(start),
                          self._zerobased(stop)) + 1

    def pop(self, i):
        return list.pop(self, self._zerobased(i))

senderle's ExtraItemList is going to have better performance, though, because it doesn't need to adjust the indices constantly nor does it have an extra layer of (non-C!) method calls between you and the data. Wish I'd thought of that approach; maybe I could profitably combine it with mine...



回答2:

Ok, well, you seem very determined. As I said above, you have to override __getitem__, __setitem__, and __delitem__. Actually you'll also have to override __getslice__, __setslice__, __delslice__ (otherwise slicing starts at 0 as usual). Then __add__, __mul__, __iadd__, __imul__ to return WonkyLists as well (otherwise concatenation won't work the way you'd want). You'll have to override index because it will return a wrong value (I tested it). Also insert, remove, and pop. Here's something to get you started:

class WonkyList(list):
    def __getitem__(self, index):
        return super(WonkyList, self).__getitem__(index - 1)
    def __setitem__(self, index, val):
        super(WonkyList, self).__setitem__(index - 1, val)
    def __delitem__(self, index):
        super(WonkyList, self).__delitem__(index - 1)

Tested:

>>> w = WonkyList(range(10))
>>> w[1]
0
>>> del w[5]
>>> w
[0, 1, 2, 3, 5, 6, 7, 8, 9]
>>> w[1] = 10
>>> w
[10, 1, 2, 3, 5, 6, 7, 8, 9]

Here are some ways it will fail until you override all the necessary methods:

>>> w
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> w[5]
4
>>> w.pop(5)
5
>>> w.insert(5, 5)
>>> w
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> del w[2:4]
>>> w
[0, 1, 4, 5, 6, 7, 8, 9]
>>> 
>>> w.index(1)
1

Or you could try something else. Have you considered, for example...

def ExtraItemList(list):
    def __init__(self, seq):
        self[0] = 0
        self[1:] = seq
    def n_items(self):
        return len(self) - 1

That seems to take care of the issues you list, and it still behaves in a basically list-like manner, even if the initializer is a little weird. Though to be honest, looking at the examples you gave, I'm starting to feel like even this is an ugly solution -- none of the functions you gave show the advantage of starting indexing from 1, they just show the bad effects of adding an extra item to the beginning of the list. Based on those examples, you should just use a regular list.

Perhaps the best approach would be to write custom get and set methods that use the desired offset. That way the lists would behave normally, but you'd have an alternative interface when needed.



回答3:

Here's a basic implementation to get you started. I thought it would be nice to generalize it, so you can have lists with indices starting at any integer you like. This led me to discover __slots__, so thank you for asking this question!

class OffsetList(list):
    __slots__ = 'offset'
    def __init__(self, init=[], offset=-1):
        super(OffsetList, self).__init__(init)
        self.offset = offset
    def __getitem__(self, key):
        return super(OffsetList, self).__getitem__(key + self.offset)
    def __setitem__(self, key, value):
        return super(OffsetList, self).__setitem__(key + self.offset, value)
    def __delitem__(self, key):
        return super(OffsetList, self).__delitem__(key + self.offset)
    def index(self, *args):
        return super(OffsetList, self).index(*args) - self.offset

>>> a = OffsetList([1,2,3])
>>> a
[1, 2, 3]
>>> a[1]
1
>>> a.index(2)
2

You may find you want to implement __getslice__, __setslice__, __delslice__ too, not to mention pop and insert.