自定义包装器,使Python列表从1开始索引

3 投票
3 回答
5136 浏览
提问于 2025-04-16 21:38

我想为Python的list类型写一个简单的封装,让它从1开始索引,而不是从0开始。我有一个比较复杂的程序,基于一些离散概率分布的持续时间数据,使用的是整数长度的桶,但我没有小于1的持续时间。总之,如果能从1开始索引,会大大简化我代码中的一些重要部分。我一开始用的是dict,但发现它的一些特性太麻烦了。

我之前从来没有为Python类写过封装,更别提内置类型了,但我觉得我想做的事情其实挺简单的。至少,我应该能做到这样的操作:

>>> 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

不过,如果我能重写一些list类的其他相关内置方法,比如index,那就更好了。

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

请分享一下你们的建议,告诉我该怎么做。我在考虑是否要用自定义类来实现,但这似乎有点过于复杂。我也欢迎任何关于有用的重写方法的推荐。

编辑:后记

我想说的是,做这个其实不太值得麻烦,之所以接受下面的回答,并不是因为我尝试按照他描述的方式去实现,而是因为他让我意识到,列表本身已经足够好了。

3 个回答

1

这是一个基本的实现,帮助你入门。我觉得把它做得更通用一些会很好,这样你就可以创建从任何整数开始的列表索引。这让我发现了__slots__,所以谢谢你提出这个问题!

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

你可能还想实现一下__getslice____setslice____delslice__,更不用说popinsert了。

1

好的,看起来你很有决心。正如我之前提到的,你需要重写几个特殊的方法:__getitem____setitem____delitem__。实际上,你还需要重写 __getslice____setslice____delslice__(否则切片操作会像平常一样从0开始)。接着,你还需要重写 __add____mul____iadd____imul__,这样才能返回 WonkyList(否则拼接操作就不能按你想要的方式工作)。你还得重写 index,因为它会返回错误的值(我测试过了)。另外,insertremovepop 也需要重写。这里有一些可以帮助你入门的内容:

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)

测试过了:

>>> 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]

在你重写所有必要的方法之前,这里有一些可能会出错的情况:

>>> 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

或者你可以尝试其他方法。比如,你有没有考虑过……

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

这个方法似乎解决了你提到的问题,而且它的行为基本上还是像列表,尽管初始化的方式有点奇怪。不过说实话,看看你给的例子,我开始觉得即使这样也不是个好办法——你给的函数并没有显示从1开始索引的好处,只是展示了在列表开头添加一个额外项的坏影响。根据这些例子,你应该直接使用普通的列表。

或许最好的方法是写自定义的 getset 方法,使用你想要的偏移量。这样列表的行为就会正常,但在需要的时候你可以有一个替代的接口。

3

这里有一个完整的(我觉得是)基于1的列表实现,它能正确处理切片(包括扩展切片)、索引、弹出元素等功能。其实,要把这些做对比你想象的要复杂一点,特别是切片和负索引的部分。事实上,我现在也不太确定它是否完全按照预期工作,所以 程序员请注意。

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 的 ExtraItemList 性能会更好,因为它不需要不断调整索引,也没有额外的(非C语言的)方法调用层在你和数据之间。我真希望我能想到这种方法;也许我可以把它和我的方法结合起来,创造出更好的效果……

撰写回答