Implementing Slicing in _Getitem_

Implementing slicing in __getitem__

The __getitem__() method will receive a slice object when the object is sliced. Simply look at the start, stop, and step members of the slice object in order to get the components for the slice.

>>> class C(object):
... def __getitem__(self, val):
... print val
...
>>> c = C()
>>> c[3]
3
>>> c[3:4]
slice(3, 4, None)
>>> c[3:4:-2]
slice(3, 4, -2)
>>> c[():1j:'a']
slice((), 1j, 'a')

How to implement slice in Python 3?

You need to use the slice.indices method. Given the length of your sequence, it returns a tuple of start, stop, step:

>>> s = slice(2, 5, None)
>>> s.indices(10)
(2, 5, 1)

>>> [x for x in range(*s.indices(10))]
[2, 3, 4]

>>> s.indices(3)
(2, 3, 1)

>>> s.indices(0)
(0, 0, 1)

Implementing __getitem__

You could use exception handling; assume key is a slice object and call the indices() method on it. If that fails it must've been an integer:

def __getitem__(self, key):
try:
return [self.somelist[i] * 5 for i in key.indices(self.length)]
except AttributeError:
# not a slice object (no `indices` attribute)
return self.somelist[key] * 5

Most use-cases for custom containers don't need to support slicing, and historically, the __getitem__ method only ever had to handle integers (for sequences, that is); the __getslice__() method was there to handle slicing instead. When __getslice__ was deprecated, for backwards compatibility and for simpler APIs it was easier to have __getitem__ handle both integers and slice objects.

And that is ignoring the fact that outside sequences, key doesn't have to be an integer. Custom classes are free to support any key type they like.

How to implement the lazy slicing of a class instance?

Instead of returning a generator, return a view. This is an object that conceptually represents the sequence of elements in question. We can do this by storing a reference to the original A instance, plus a range that encodes the instances. When we index into the view, we can ask the range which original index (or indices) are involved.

Supposing we set up the general structure:

class A(list):
def __init__(self, n):
super().__init__()
self[:] = [i for i in range(0, n)]

def _at(self, idx):
# custom logic here to return the correct value for self[idx]

def __getitem__(self, idx):
if isinstance(idx, int):
return self._at(idx)
elif isinstance(idx, slice):
# The `indices` method of a slice object converts to
# start, stop, step values which we can use to construct a range.
return A_view(self, range(*idx.indices(len(self))))
else:
raise TypeError # this is not an appropriate use for `assert`

Then our view could look like:

class A_view:
def __init__(self, original, indices):
self._original, self._indices = original, indices

def __getitem__(self, idx):
if isinstance(idx, int):
return self._original[self._indices[idx]]
elif isinstance(idx, slice):
return A_view(self._original, self._indices[idx])
else:
raise TypeError

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

The idea is that if we receive an integer index, we translate it through the range object into an index for the original A, and call back to its __getitem__ (with an integer this time). If we receive another slice, we use it to slice our range into a sub-range, and make another view.

Note that your A class should already be iterable, due to inheriting from list. To make the view iterable (and also get the in operator, forward and reverse iteration, .index and .count methods for free), you can inherit from collections.abc.Sequence. You just need a __getitem__ and __len__ - both easy to implement, as above - and the base class will do the rest (while also advertising to isinstance that your class has these properties).

How do __getitem__, __setitem__, work with slices?

The problem is that you're subclassing a builtin, and so have to deal with a few wrinkles. Before I delve into that issue, I'll go straight to the "modern" way:

How will things work in the future? Is there a "list" class - that I am not aware of - that I could extend and would not present this inconvenience?

Yes, there's the stdlib Abstract Base Classes. You can avoid the ugly complications caused by subclassing builtin list by using the ABCs instead. For something list-like, try subclassing MutableSequence:

from collections import MutableSequence

class MyList(MutableSequence):
...

Now you should only need to deal with __getitem__ and friends for slicing behaviour.


If you want to push ahead with subclassing the builtin list, read on...

Your guess is correct, you will need to override __getslice__ and __setslice__. The language reference explains why and you already saw that:

However, built-in types in CPython currently still implement __getslice__(). Therefore, you have to override it in derived classes when implementing slicing.

Note that l[3:7] will hook into __getslice__, whereas the otherwise equivalent l[3:7:] will hook into __getitem__, so you have to handle the possibility of receiving slices in both methods... groan!

How to implement a secondary custom method for object slicing, other than __getitem__ in Python

The [ ] syntax requires a proper object with a __getitem__ method. In order to have a "slice method", use a property that returns a helper which supports slicing.

The helper simply holds a reference to the actual parent object, and defines a __getitem__ with the desired behaviour:

class VectorIloc:
def __init__(self, parent):
self.parent = parent

# custom logic for desired "iloc" behaviour
def __getitem__(self, item):
key = list(self.parent.dictionary)[item]
return self.parent[key]

On the actual class, merely define the desired "method" as a property that returns the helper or as an attribute:

class Vector:
def __init__(self, map_object: dict):
self.dictionary = map_object
# if .iloc is used often
# self.iloc = VectorIloc(self)

def __getitem__(self, key):
return self.dictionary[key]

# if .iloc is used rarely
@property
def iloc(self):
return VectorIloc(self)

Whether to use a property or an attribute is an optimisation that trades memory for performance: an attribute constructs and stores the helper always, while a property constructs it only on-demand but on each access. A functools.cached_property can be used as a middle-ground, creating the attribute on first access.

The property is advantageous when the helper is used rarely per object, and especially if it often is not used at all.


Now, when calling vector.iloc[3], the vector.iloc part provides the helper and the [3] part invoces the helper's __getitem__.

>>> vector = Vector({0:0, 1: 1, 2: 2, "three": 3})
>>> vector.iloc[3]
3

Python slice objects and __getitem__

The Python grammar defines when you can use the slice operator:

trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME
subscriptlist: subscript (',' subscript)* [',']
subscript: test | [test] ':' [test] [sliceop]
sliceop: ':' [test]

test is pretty much any expression, but it is only inside a subscriptlist that you can use the slice operator. So yes, the square brackets when used for subscripting are what matter, but square brackets used for lists won't magically allow you to write a slice, nor can you put a slice inside an arbitrary expression that just happens to be inside a subscript.

If you want slices when you aren't subscripting something you'll have to write slice(a,b,c).

How does list slicing hook into __getitem__?

The language reference says it all. Specifically:

Deprecated since version 2.0 : Support slice objects as parameters __getitem__() method. (However, built-in types in CPython currently still implement __getslice__(). Therefore, you have to override it in derived classes when implementing slicing.)

and:

Called to implement evaluation of self[i:j] ...

Note it doesn't handle self[i:j:] ...



Related Topics



Leave a reply



Submit