124

I am trying to implement slice functionality for a class I am making that creates a vector representation.

I have this code so far, which I believe will properly implement the slice but whenever I do a call like v[4] where v is a vector python returns an error about not having enough parameters. So I am trying to figure out how to define the getitem special method in my class to handle both plain indexes and slicing.

def __getitem__(self, start, stop, step):
    index = start
    if stop == None:
        end = start + 1
    else:
        end = stop
    if step == None:
        stride = 1
    else:
        stride = step
    return self.__data[index:end:stride]
martineau
  • 99,260
  • 22
  • 139
  • 249
nicotine
  • 2,169
  • 3
  • 17
  • 15

5 Answers5

130

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')
Ignacio Vazquez-Abrams
  • 699,552
  • 132
  • 1,235
  • 1,283
  • 11
    Note: for extending builtins types like list or tuple you must implements `__getslice__` for python 2.X versions. see https://docs.python.org/2/reference/datamodel.html#object.__getslice__ – gregorySalvan Aug 28 '14 at 05:17
  • @gregorySalvan: Doesn't that compatibility example below that section just recurse? – Eric Aug 19 '16 at 19:10
  • 3
    @Eric: No, because the presence of the second colon bypasses `__get/set/delslice__`. It's pretty subtle, though. – user2357112 supports Monica May 25 '17 at 01:41
  • @user2357112: Wow, completely missed that second colon - thanks! – Eric May 25 '17 at 10:31
  • @alancalvitti IIRC, that's for creating new-style classes in Python 2. – wjandrea Apr 09 '19 at 17:07
  • Note that `__getitem__` *can* receive a `slice`, but could also receive an `int` as in `c[0]`. see https://docs.python.org/3/reference/datamodel.html#object.__getitem__ – Luca Dec 12 '19 at 17:16
67

I have a "synthetic" list (one where the data is larger than you would want to create in memory) and my __getitem__ looks like this:

def __getitem__( self, key ) :
    if isinstance( key, slice ) :
        #Get the start, stop, and step from the slice
        return [self[ii] for ii in xrange(*key.indices(len(self)))]
    elif isinstance( key, int ) :
        if key < 0 : #Handle negative indices
            key += len( self )
        if key < 0 or key >= len( self ) :
            raise IndexError, "The index (%d) is out of range."%key
        return self.getData(key) #Get the data from elsewhere
    else:
        raise TypeError, "Invalid argument type."

The slice doesn't return the same type, which is a no-no, but it works for me.

Walter Nissen
  • 14,485
  • 4
  • 24
  • 26
  • 1
    Shouldn't if key >= len( self ) be if key < 0 or key >= len( self ) ? What if a key < -len(self) is passed? – estan Jun 01 '16 at 17:50
24

How to define the getitem class to handle both plain indexes and slicing?

Slice objects gets automatically created when you use a colon in the subscript notation - and that is what is passed to __getitem__. Use isinstance to check if you have a slice object:

from __future__ import print_function

class Sliceable(object):
    def __getitem__(self, subscript):
        if isinstance(subscript, slice):
            # do your handling for a slice object:
            print(subscript.start, subscript.stop, subscript.step)
        else:
            # Do your handling for a plain index
            print(subscript)

Say we were using a range object, but we want slices to return lists instead of new range objects (as it does):

>>> range(1,100, 4)[::-1]
range(97, -3, -4)

We can't subclass range because of internal limitations, but we can delegate to it:

class Range:
    """like builtin range, but when sliced gives a list"""
    __slots__ = "_range"
    def __init__(self, *args):
        self._range = range(*args) # takes no keyword arguments.
    def __getattr__(self, name):
        return getattr(self._range, name)
    def __getitem__(self, subscript):
        result = self._range.__getitem__(subscript)
        if isinstance(subscript, slice):
            return list(result)
        else:
            return result

r = Range(100)

We don't have a perfectly replaceable Range object, but it's fairly close:

>>> r[1:3]
[1, 2]
>>> r[1]
1
>>> 2 in r
True
>>> r.count(3)
1

To better understand the slice notation, here's example usage of Sliceable:

>>> sliceme = Sliceable()
>>> sliceme[1]
1
>>> sliceme[2]
2
>>> sliceme[:]
None None None
>>> sliceme[1:]
1 None None
>>> sliceme[1:2]
1 2 None
>>> sliceme[1:2:3]
1 2 3
>>> sliceme[:2:3]
None 2 3
>>> sliceme[::3]
None None 3
>>> sliceme[::]
None None None
>>> sliceme[:]
None None None

Python 2, be aware:

In Python 2, there's a deprecated method that you may need to override when subclassing some builtin types.

From the datamodel documentation:

object.__getslice__(self, i, j)

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

This is gone in Python 3.

Community
  • 1
  • 1
Aaron Hall
  • 291,450
  • 75
  • 369
  • 312
7

The correct way to do this is to have __getitem__ take one parameter, which can either be a number, or a slice object.

See:

http://docs.python.org/library/functions.html#slice

http://docs.python.org/reference/datamodel.html#object.__getitem__

blueshift
  • 6,325
  • 1
  • 33
  • 59
carl
  • 47,134
  • 17
  • 71
  • 81
7

To extend Aaron's answer, for things like numpy, you can do multi-dimensional slicing by checking to see if given is a tuple:

class Sliceable(object):
    def __getitem__(self, given):
        if isinstance(given, slice):
            # do your handling for a slice object:
            print("slice", given.start, given.stop, given.step)
        elif isinstance(given, tuple):
            print("multidim", given)
        else:
            # Do your handling for a plain index
            print("plain", given)

sliceme = Sliceable()
sliceme[1]
sliceme[::]
sliceme[1:, ::2]

```

Output:

('plain', 1)
('slice', None, None, None)
('multidim', (slice(1, None, None), slice(None, None, 2)))
Eric Cousineau
  • 1,161
  • 7
  • 14
  • As a minor follow-up, here [is an example](https://github.com/EricCousineau-TRI/repro/blob/62151af/bindings/mlmodule/NumPyProxy.m#L83) of employing this to map between MATLAB indexing and NumPy indexing (which is presently not supported in MATLAB R2016b), with an [example usage](https://github.com/EricCousineau-TRI/repro/blob/62151af/bindings/mlmodule/test/example_py_slice.m#L43) of it. – Eric Cousineau May 30 '17 at 05:02