Can I create a "view" on a Python list?

Viewed 16865

I have a large list l. I want to create a view from element 4 to 6. I can do it with sequence slice.

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

However lv is a copy of a slice of l. If I change the underlying list, lv does not reflect the change.

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

Vice versa I want modification on lv reflect in l as well. Other than that the list size are not going to be changed.

I'm not looking forward to build a big class to do this. I'm just hoping other Python gurus may know some hidden language trick. Ideally I hope it can be like pointer arithmetic in C:

int lv[] = l + 3;
10 Answers

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

Above link is a solution based on python 3 range ability to be sliced and indexed in constant time.

It supports slicing, equality comparsion, string casting (__str__), and reproducers (__repr__), but doesn't support assigment.

Creating a SliceableSequenceView of a SliceableSequenceView won't slow down access times as this case is detected.

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

It's actually not too difficult to implement this yourself using range.* You can slice a range and it does all of the complicated arithmetic for you:

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

So you just need a class of object that contains a reference to the original sequence, and a range. Here is the code for such a class (not too big, I hope):

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)

(This was bodged together in about 5 minutes, so make sure you test it thoroughly before using it anywhere important.)

Usage:

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

* in Python 3

Related