我可以为Python列表创建一个“视图”吗?

63

我有一个大列表l。 我想创建从第4个元素到第6个元素的视图。 我可以使用序列切片完成。

>>> l = range(10)
>>> lv = l[3:6]
>>> lv
[3, 4, 5]

然而,lvl 切片的副本。如果我更改了基础列表,lv 不会反映出这种变化。

>>> l[4] = -1
>>> lv
[3, 4, 5]
反之亦然,我希望对lv的修改也能反映在l中。除此之外,列表的大小不会改变。 我不想建立一个庞大的类来完成这个操作。我只是希望其他Python达人知道一些隐藏的语言技巧。理想情况下,我希望它可以像C语言中的指针算术一样简单。
int lv[] = l + 3;

@robert 怎么办?memoryview 仅适用于具有缓冲区接口的对象,而列表不是其中之一。 - zegkljan
在此提供的示例中,您应该使用bytearray而不是列表。您还可以将列表包装在bytearray中。 - robert
由于memoryview文档没有链接到它,因此请参考缓冲区协议 - Kevin J. Chase
10个回答

41

Python标准库中没有“列表切片”类(也没有内置的)。因此,您确实需要一个类,尽管它不需要很大--特别是如果您只需要一个“只读”和“紧凑”的切片。例如:

import collections

class ROListSlice(collections.Sequence):

    def __init__(self, alist, start, alen):
        self.alist = alist
        self.start = start
        self.alen = alen

    def __len__(self):
        return self.alen

    def adj(self, i):
        if i<0: i += self.alen
        return i + self.start

    def __getitem__(self, i):
        return self.alist[self.adj(i)]

这有一些限制(不支持“切片一个切片”),但在大多数情况下可能是可以的。

要让这个序列可读/写,需要添加__setitem____delitem__insert

class ListSlice(ROListSlice):

    def __setitem__(self, i, v):
        self.alist[self.adj(i)] = v

    def __delitem__(self, i, v):
        del self.alist[self.adj(i)]
        self.alen -= 1

    def insert(self, i, v):
        self.alist.insert(self.adj(i), v)
        self.alen += 1

3
但是如果你执行 alist.insert(0, something) 操作,那么这个切片就会移动位置!这可能会成为一个问题,也可能不会...... - Jochen Ritzel
@Amber,__slice__在Python中不是一种特殊的方法。对于切片操作,会调用__getindex__,__setindex__,__delindex__ 方法,因此您需要进行类型检查并进行调整(对获取来说可能比较容易,因为您的方法将委托好事情,但对于设置和删除来说则更困难)。 - Alex Martelli
1
@Alex:嗯,我记得有办法覆盖切片操作(比如允许二维切片之类的),但也有可能我记错了 :)。 - Amber
1
@Amber, 当然你可以“覆盖切片”——只需要覆盖__getitem__(对于带有可变实例的类型,也许还要覆盖set和del),并在“索引”参数上进行类型检查/类型转换(例如,为了允许a[1:2,3:4],你需要处理接收到一个包含两个项的元组作为“索引”参数,两个项都是切片对象)。 - Alex Martelli
@Amber,虽然这对于__getitem__可以工作(慢,因为它为每个索引创建了一个冗余切片,并且collections.Sequence大量使用索引--查看其源代码),但对于__setitem__则不行,如果您只能获取而不能设置切片,那将是非常奇怪的。因此,对于可变序列,您必须执行类型检查和调整(可能在def adj中,这样至少您只需要编写一次代码,但仍然是相当微妙的代码,而且OP说他不想要“一个大类”;-)--对于不可变序列,只有在您真正关心性能时才需要进行调整。 - Alex Martelli
显示剩余5条评论

32

或许只需使用一个numpy数组:

In [19]: import numpy as np

In [20]: l=np.arange(10)

基本切片 numpy 数组返回一个视图,而不是副本:

In [21]: lv=l[3:6]

In [22]: lv
Out[22]: array([3, 4, 5])

修改 l 会影响到 lv

In [23]: l[4]=-1

In [24]: lv
Out[24]: array([ 3, -1,  5])

修改 lv 会影响到 l

In [25]: lv[1]=4

In [26]: l
Out[26]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

9
你可以通过创建自己的生成器,使用原始列表引用来实现这一点。
l = [1,2,3,4,5]
lv = (l[i] for i in range(1,4))

lv.next()   # 2
l[2]=-1
lv.next()   # -1
lv.next()   # 4

然而,由于这是一个生成器,你只能按顺序向前遍历列表一次,如果你删除的元素比使用range请求的元素数量还多,它就会爆炸。


7

通过子类化more_itertools.SequenceView,可以通过改变序列来影响视图,反之亦然。

代码

import more_itertools as mit


class SequenceView(mit.SequenceView):
    """Overload assignments in views."""
    def __setitem__(self, index, item):
        self._target[index] = item

演示

>>> seq = list(range(10))
>>> view = SequenceView(seq)
>>> view
SequenceView([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

>>> # Mutate Sequence -> Affect View
>>> seq[6] = -1
>>> view[5:8]
[5, -1, 7]

>>> # Mutate View -> Affect Sequence
>>> view[5] = -2
>>> seq[5:8]
[-2, -1, 7]

more_itertools 是一个第三方库。可以通过以下命令进行安装:> pip install more_itertools


3
感谢让我发现 more_itertools,但是我不会使用你的代码。 - loxaxs
没问题。more_itertools 是一个很棒的工具箱。我鼓励大家去探索它。 - pylang

6

https://gist.github.com/mathieucaroff/0cf094325fb5294fb54c6a577f05a2c1

上面的链接是基于Python 3范围能够在常数时间内进行切片和索引的解决方案。
它支持切片、相等比较、字符串转换(__str__)和重现(__repr__),但不支持赋值。
创建一个SliceableSequenceView的SliceableSequenceView不会减慢访问速度,因为这种情况已经被检测到了。

sequenceView.py

# stackoverflow.com/q/3485475/can-i-create-a-view-on-a-python-list

try:
    from collections.abc import Sequence
except ImportError:
    from collections import Sequence # pylint: disable=no-name-in-module

class SliceableSequenceView(Sequence):
    """
    A read-only sequence which allows slicing without copying the viewed list.
    Supports negative indexes.

    Usage:
        li = list(range(100))
        s = SliceableSequenceView(li)
        u = SliceableSequenceView(li, slice(1,7,2))
        v = s[1:7:2]
        w = s[-99:-93:2]
        li[1] += 10
        assert li[1:7:2] == list(u) == list(v) == list(w)
    """
    __slots__ = "seq range".split()
    def __init__(self, seq, sliced=None):
        """
        Accept any sequence (such as lists, strings or ranges).
        """
        if sliced is None:
            sliced = slice(len(seq))
        ls = looksSliceable = True
        ls = ls and hasattr(seq, "seq") and isinstance(seq.seq, Sequence)
        ls = ls and hasattr(seq, "range") and isinstance(seq.range, range)
        looksSliceable = ls
        if looksSliceable:
            self.seq = seq.seq
            self.range = seq.range[sliced]
        else:
            self.seq = seq
            self.range = range(len(seq))[sliced]

    def __len__(self):
        return len(self.range)

    def __getitem__(self, i):
        if isinstance(i, slice):
            return SliceableSequenceView(self.seq, i)
        return self.seq[self.range[i]]

    def __str__(self):
        r = self.range
        s = slice(r.start, r.stop, r.step)
        return str(self.seq[s])

    def __repr__(self):
        r = self.range
        s = slice(r.start, r.stop, r.step)
        return "SliceableSequenceView({!r})".format(self.seq[s])

    def equal(self, otherSequence):
        if self is otherSequence:
            return True
        if len(self) != len(otherSequence):
            return False
        for v, w in zip(self, otherSequence):
            if v != w:
                return False
        return True

4

一旦你从一个列表中取出一个片段,你将创建一个新的列表。好吧,它将包含相同的对象,因此就列表对象而言,它将是相同的,但如果更改切片,则原始列表不会受到影响。

如果您真的想要创建一个可修改的视图,可以想象一个基于 collection.MutableSequence 的新类。

这可能是一个完整特色的子列表的起点--它正确地处理切片索引,但至少缺乏负索引处理的规范:

class Sublist(collections.MutableSequence):
    def __init__(self, ls, beg, end):
        self.ls = ls
        self.beg = beg
        self.end = end
    def __getitem__(self, i):
        self._valid(i)
        return self.ls[self._newindex(i)]
    def __delitem__(self, i):
        self._valid(i)
        del self.ls[self._newindex(i)]
    def insert(self, i, x):
        self._valid(i)
        self.ls.insert(i+ self.beg, x)
    def __len__(self):
        return self.end - self.beg
    def __setitem__(self, i, x):
        self.ls[self._newindex(i)] = x
    def _valid(self, i):
        if isinstance(i, slice):
            self._valid(i.start)
            self._valid(i.stop)
        elif isinstance(i, int):
            if i<0 or i>=self.__len__():
                raise IndexError()
        else:
            raise TypeError()
    def _newindex(self, i):
        if isinstance(i, slice):
            return slice(self.beg + i.start, self.beg + i.stop, i.step)
        else:
            return i + self.beg

例子:

>>> a = list(range(10))
>>> s = Sublist(a, 3, 8)
>>> s[2:4]
[5, 6]
>>> s[2] = 15
>>> a
[0, 1, 2, 3, 4, 15, 6, 7, 8, 9]

这是对另一个被关闭为重复的问题的直接回答。由于这里的其他答案也相关,我更喜欢在这里添加它。 - Serge Ballesta

1

编辑:对象参数必须是支持缓冲区调用接口的对象(例如字符串、数组和缓冲区)。 - 所以,不幸的是,不能这样做。

我认为缓冲区类型是你要找的。

从链接页面粘贴示例:

>>> s = bytearray(1000000)   # a million zeroed bytes
>>> t = buffer(s, 1)         # slice cuts off the first byte
>>> s[1] = 5                 # set the second element in s
>>> t[0]                     # which is now also the first element in t!
'\x05' 

1
Python 3 中没有 buffer() 内置函数。可以使用 memoryview() 替代。 - jfs
1
此外,这会检查区域的内存字节 - Python列表包含对象(在内存中是指向对象的指针),因此 - 明确地说,这将是一种非常错误的方法 - 人们必须使用ctypes,并重新执行所有指针间接工作,就像他在编写C代码一样,而Python则免费完成了这项工作。 - jsbueno

0

你不能像这样进行编辑:

shiftedlist = type('ShiftedList',
                   (list,),
                   {"__getitem__": lambda self, i: list.__getitem__(self, i + 3)}
                  )([1, 2, 3, 4, 5, 6])

本质上来说,这是一行代码,它并不符合 Pythonic 的风格,但这就是基本的要点。

编辑:我已经晚些意识到这行代码不可行,因为 list() 会对传入的列表进行浅拷贝。所以这最终将与仅仅切片列表的方式差不多。实际上还不如切片,因为缺少对 __len__ 的覆盖。你需要使用代理类;详见 Mr. Martelli's answer


0

使用range实际上并不难实现。您可以对范围进行切片,它会为您完成所有复杂的算术运算:

>>> range(20)[10:]
range(10, 20)
>>> range(10, 20)[::2]
range(10, 20, 2)
>>> range(10, 20, 2)[::-3]
range(18, 8, -6)

所以你只需要一个包含对原始序列引用和范围的对象类。这是这样一个类的代码(希望不会太大):

class SequenceView:

    def __init__(self, sequence, range_object=None):
        if range_object is None:
            range_object = range(len(sequence))
        self.range    = range_object
        self.sequence = sequence

    def __getitem__(self, key):
        if type(key) == slice:
            return SequenceView(self.sequence, self.range[key])
        else:
            return self.sequence[self.range[key]]

    def __setitem__(self, key, value):
        self.sequence[self.range[key]] = value

    def __len__(self):
        return len(self.range)

    def __iter__(self):
        for i in self.range:
            yield self.sequence[i]

    def __repr__(self):
        return f"SequenceView({self.sequence!r}, {self.range!r})"

    def __str__(self):
        if type(self.sequence) == str:
            return ''.join(self)
        elif type(self.sequence) in (list, tuple):
            return str(type(self.sequence)(self))
        else:
            return repr(self)

这是在大约5分钟内拼凑出来的,因此在将其用于任何重要场合之前,请务必进行彻底测试。

用法:

>>> p = list(range(10))
>>> q = SequenceView(p)[3:6]
>>> print(q)
[3, 4, 5]
>>> q[1] = -1
>>> print(q)
[3, -1, 5]
>>> print(p)
[0, 1, 2, 3, -1, 5, 6, 7, 8, 9]

* 在 Python 3 中


-3

如果您要按顺序访问“视图”,那么您可以使用itertools.islice(..)有关更多信息,请参阅文档

l = [1, 2, 3, 4, 5]
d = [1:3] #[2, 3]
d = itertools.islice(2, 3) # iterator yielding -> 2, 3

在切片中,您无法访问单个元素以更改它们,如果您确实更改了列表,则必须重新调用isclice(..)。


7
代码示例连语法都不正确。 - Alexey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接