【问题标题】:Can I create a "view" on a Python list?我可以在 Python 列表上创建“视图”吗?
【发布时间】:2011-03-29 23:12:10
【问题描述】:

我有一个很大的列表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 仅适用于具有缓冲区接口的对象,而列表不是其中之一。
  • 在此处提供的示例中,您应该使用bytearray 而不是列表。您也可以将列表包装在bytearray
  • buffer protocol,因为memoryview 文档没有链接到它。

标签: python arrays list slice


【解决方案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

【讨论】:

  • 你能做一些像def __slice__(self, *args, **kwargs): return (self.alist[self.start:self.start+self.alen]).__slice__(*args, **kwargs)这样的东西来支持像切片这样的东西吗?基本上将请求传递给按需创建的切片。
  • 但是如果你这样做alist.insert(0, something),切片就会移动!这可能是也可能不是问题...
  • @Amber, __slice__ 不是 Python 中的特殊方法。切片会导致调用__getindex____setindex____delindex__,因此您必须进行类型检查和调整(更容易获取,因为您的方法会将事情委派好——尽管设置和删除更难) .
  • @Alex:嗯。我可以发誓有办法覆盖切片(例如,允许二维切片之类的东西)。但我可能是错的。 :)
  • @Amber,当然你可以“覆盖切片”——你可以通过覆盖 __getitem__ 来做到这一点(对于具有可变实例的类型,可能还有 set 和 del ),然后输入-检查/类型切换“索引”参数(例如,为了允许a[1:2,3:4],您处理接收作为“索引”参数的元组,其中包含两个项目,它们都是切片对象)。
【解决方案2】:

也许只使用一个 numpy 数组:

In [19]: import numpy as np

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

基本切片numpy数组returns a view,不是副本:

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 是第三方库。通过&gt; pip install more_itertools安装。

      【讨论】:

      • 加一个让我发现more_itertools,虽然我不会使用你的代码
      • 不用担心。 more_itertools 是一个很棒的工具箱。我鼓励人们去探索它。
      【解决方案5】:

      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
      

      【讨论】:

        【解决方案6】:

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

        我认为buffer type 是您正在寻找的。​​p>

        从链接页面粘贴示例:

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

        【讨论】:

        • Python 3 中没有内置的buffer()。可以使用memoryview()
        • 此外,这会检查该区域的内存字节 - Python 列表确实包含对象(“内存中”是指向对象的指针)所以 - 明确地说,这将是一种非常错误的方法 - 一个将不得不使用 ctypes 并重做所有指针间接工作,就好像他在用 C 编码一样,Python 是免费的
        【解决方案7】:

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

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

        【讨论】:

        • 这是对另一个问题的直接回答,该问题作为此问题的副本关闭。由于此处的其他答案是相关的,我更喜欢在此处添加它
        【解决方案8】:

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

        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

        【讨论】:

          【解决方案9】:

          使用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 中

          【讨论】:

            【解决方案10】:

            如果您要按顺序访问“视图”,则只需使用 itertools.islice(..)You can see the documentation for more info

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

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

            【讨论】:

            • 代码示例甚至在语法上都不正确。
            猜你喜欢
            • 2020-01-12
            • 2021-05-18
            • 2021-09-11
            • 2023-02-21
            • 1970-01-01
            • 1970-01-01
            • 2017-12-18
            相关资源
            最近更新 更多