我可以在 Python 列表上创建“视图”吗?

2025-03-05 09:18:00
admin
原创
6
摘要:问题描述:我有一个大列表l。我想创建一个从元素 4 到 6 的视图。我可以使用序列切片来实现。>>> l = range(10) >>> lv = l[3:6] >>> lv [3, 4, 5] 但是lv是 的一个切片的副本l。如果我更改了底层列表,lv则...

问题描述:

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

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

但是lv是 的一个切片的副本l。如果我更改了底层列表,lv则不会反映出该更改。

>>> l[4] = -1
>>> lv
[3, 4, 5]

反之亦然,我也希望修改lv反映l。除此之外,列表大小不会改变。

我并不期待建立一个大类来做这件事。我只是希望其他 Python 大师可能知道一些隐藏的语言技巧。理想情况下,我希望它可以像 C 中的指针算法一样:

int lv[] = l + 3;

解决方案 1:

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

这有一些限制(不支持“切片”)但对于大多数目的来说可能没问题。

要使此序列成为 r/w 序列,您需要添加__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

解决方案 2:

也许只是使用一个 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])

解决方案 3:

您可以通过使用原始列表引用创建自己的生成器来实现这一点。

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

解决方案 4:

通过改变序列来子类化以影响视图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

解决方案 5:

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

以上链接是一个基于 python 3 范围能力的解决方案,可以在恒定时间内进行切片和索引。

它支持切片、相等性比较、字符串转换(__str__)和复制器(__repr__),但不支持分配。

创建 SliceableSequenceView 的 SliceableSequenceView 不会减慢访问时间,因为检测到了这种情况。

序列视图.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

解决方案 6:

只要您从列表中取出一个切片,就会创建一个新列表。好的,它将包含相同的对象,因此只要列表中的对象是相同的,但如果您修改切片,原始列表将保持不变。

如果你真的想创建一个可修改的视图,你可以想象一个基于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]

解决方案 7:

实际上,使用 .* 自己实现这一点并不太难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中

解决方案 8:

编辑: The object argument must be an object that supports the buffer call interface (such as strings, arrays, and buffers). - 所以很遗憾,不行。

我认为缓冲区类型就是您正在寻找的。

从链接页面粘贴示例:

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

解决方案 9:

您可以编辑:不要做类似的事情

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

由于它本质上是一行代码,因此不太符合 Python 风格,但这就是其基本要点。

编辑:我后来才意识到这不起作用,因为list()本质上是对传递的列表进行浅拷贝。因此,这最终或多或少与仅对列表进行切片相同。实际上更少,因为缺少 的覆盖__len__。您需要使用代理类;有关详细信息,请参阅Martelli 先生的回答。

解决方案 10:

警告:该代码仅供研究之用。我不建议使用它。

在 CPython 中确实可以做到这一点。您可以“侵入”内部值,让一个列表引用另一个列表。但是,这种方法并不可靠,并且随时可能失效。如果您更改视图的大小或做错事,它也可能会出现段错误。

from ctypes import POINTER, cast, c_ssize_t, c_void_p, sizeof

SIZE_OFFSET = sizeof(c_ssize_t) + sizeof(c_void_p)
DATA_OFFSET = SIZE_OFFSET + sizeof(c_ssize_t)


def _get_size_pointer(obj):
    return cast(c_void_p(id(obj) + SIZE_OFFSET), POINTER(c_ssize_t))


def _get_data_pointer(obj):
    return cast(c_void_p(id(obj) + DATA_OFFSET), POINTER(c_void_p))


def listview_create(li, start, stop):
    size = len(li)

    new_li = []

    if stop <= 0 or start >= stop or start >= size:
        return new_li

    if stop > size:
        stop = size
    if start < 0:
        start = 0

    _get_size_pointer(new_li)[0] = stop - start
    _get_data_pointer(new_li)[0] = _get_data_pointer(li)[0] + sizeof(c_void_p) * start
    return new_li


def listview_destroy(view):
    _get_size_pointer(view)[0] = 0
    _get_data_pointer(view)[0] = 0


if __name__ == "__main__":
    l = list(range(10))

    lv = listview_create(l, 3, 6)
    print(lv)

    l[4] = -1
    print(lv)

    listview_destroy(lv)

输出(CPython 3.11):

[3, 4, 5]
[3, -1, 5]

解决方案 11:

如果您要按顺序访问“视图”,那么您只需使用 itertools.islice(..)您可以查看文档以获取更多信息。

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

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

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1325  
  IPD(Integrated Product Development)流程作为一种先进的产品开发管理模式,在众多企业中得到了广泛应用。它涵盖了从产品概念产生到产品退市的整个生命周期,通过整合跨部门团队、优化流程等方式,显著提升产品开发的效率和质量,进而为项目的成功奠定坚实基础。深入探究IPD流程的五个阶段与项目成功之间...
IPD流程分为几个阶段   4  
  华为作为全球知名的科技企业,其成功背后的管理体系备受关注。IPD(集成产品开发)流程作为华为核心的产品开发管理模式,其中的创新管理与实践更是蕴含着丰富的经验和深刻的智慧,对众多企业具有重要的借鉴意义。IPD流程的核心架构IPD流程旨在打破部门墙,实现跨部门的高效协作,将产品开发视为一个整体的流程。它涵盖了从市场需求分析...
华为IPD是什么   3  
  IPD(Integrated Product Development)研发管理体系作为一种先进的产品开发模式,在众多企业的发展历程中发挥了至关重要的作用。它不仅仅是一套流程,更是一种理念,一种能够全方位提升企业竞争力,推动企业持续发展的有效工具。深入探究IPD研发管理体系如何助力企业持续发展,对于众多渴望在市场中立足并...
IPD管理流程   3  
  IPD(Integrated Product Development)流程管理旨在通过整合产品开发流程、团队和资源,实现产品的快速、高质量交付。在这一过程中,有效降低成本是企业提升竞争力的关键。通过优化IPD流程管理中的各个环节,可以在不牺牲产品质量和性能的前提下,实现成本的显著降低,为企业创造更大的价值。优化产品规划...
IPD流程分为几个阶段   4  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用