Indexing and Slicing: __getitem__ and __setitem__

Our first method set allows your classes to mimic some of the behaviors of sequences and mappings. If defined in a class (or inherited by it), the __getitem__ method is called automatically for instance-indexing operations. When an instance X appears in an indexing expression like X[i], Python calls the __getitem__ method inherited by the instance, passing X to the first argument and the index in brackets to the second argument.

For example, the following class returns the square of an index value—atypical perhaps, but illustrative of the mechanism in general:

>>> class Indexer:
        def __getitem__(self, index):
            return index ** 2

>>> X = Indexer()
>>> X[2]                                # X[i] calls X.__getitem__(i)
4

>>> for i in range(5):
        print(X[i], end=' ')            # Runs __getitem__(X, i) each time

0 1 4 9 16

Intercepting Slices

Interestingly, in addition to indexing, __getitem__ is also called for slice expressions—always in 3.X, and conditionally in 2.X if you don’t provide more specific slicing methods. Formally speaking, built-in types handle slicing the same way. Here, for example, is slicing at work on a built-in list, using upper and lower bounds and a stride (see Chapter 7 if you need a refresher on slicing):

>>> L = [5, 6, 7, 8, 9]
>>> L[2:4]                              # Slice with slice syntax: 2..(4-1)
[7, 8]
>>> L[1:]
[6, 7, 8, 9]
>>> L[:-1]
[5, 6, 7, 8]
>>> L[::2]
[5, 7, 9]

Really, though, slicing bounds are bundled up into a slice object and passed to the list’s implementation of indexing. In fact, you can always pass a slice object manually—slice syntax is mostly syntactic sugar for indexing with a slice object:

>>> L[slice(2, 4)]                      # Slice with slice objects
[7, 8]
>>> L[slice(1, None)]
[6, 7, 8, 9]
>>> L[slice(None, −1)]
[5, 6, 7, 8]
>>> L[slice(None, None, 2)]
[5, 7, 9]

This matters in classes with a __getitem__ method—in 3.X, the method will be called both for basic indexing (with an index) and for slicing (with a slice object). Our previous class won’t handle slicing because its math assumes integer indexes are passed, but the following class will. When called for indexing, the argument is an integer as before:

>>> class Indexer:
        data = [5, 6, 7, 8, 9]
        def __getitem__(self, index):   # Called for index or slice
            print('getitem:', index)
            return self.data[index]     # Perform index or slice

>>> X = Indexer()
>>> X[0]                                # Indexing sends __getitem__ an integer
getitem: 0
5
>>> X[1]
getitem: 1
6
>>> X[-1]
getitem: −1
9

When called for slicing, though, the method receives a slice object, which is simply passed along to the embedded list indexer in a new index expression:

>>> X[2:4]                              # Slicing sends __getitem__ a slice object
getitem: slice(2, 4, None)
[7, 8]
>>> X[1:]
getitem: slice(1, None, None)
[6, 7, 8, 9]
>>> X[:-1]
getitem: slice(None, −1, None)
[5, 6, 7, 8]
>>> X[::2]
getitem: slice(None, None, 2)
[5, 7, 9]

Where needed, __getitem__ can test the type of its argument, and extract slice object bounds—slice objects have attributes start, stop, and step, any of which can be None if omitted:

>>> class Indexer:
        def __getitem__(self, index):
            if isinstance(index, int):               # Test usage mode
                print('indexing', index)
            else:
                print('slicing', index.start, index.stop, index.step)

>>> X = Indexer()
>>> X[99]
indexing 99
>>> X[1:99:2]
slicing 1 99 2
>>> X[1:]
slicing 1 None None

If used, the __setitem__ index assignment method similarly intercepts both index and slice assignments—in 3.X (and usually in 2.X) it receives a slice object for the latter, which may be passed along in another index assignment or used directly in the same way:

class IndexSetter:
    def __setitem__(self, index, value):    # Intercept index or slice assignment
        ...
        self.data[index] = value            # Assign index or slice

In fact, __getitem__ may be called automatically in even more contexts than indexing and slicing—it’s also an iteration fallback option, as we’ll see in a moment. First, though, let’s take a quick look at 2.X’s flavor of these operations for 2.X readers, and clarify a potential point of confusion in this category.

Slicing and Indexing in Python 2.X

In Python 2.X only, classes can also define __getslice__ and __setslice__ methods to intercept slice fetches and assignments specifically. If defined, these methods are passed the bounds of the slice expression, and are preferred over __getitem__ and __setitem__ for two-limit slices. In all other cases, though, this context works the same as in 3.X; for example, a slice object is still created and passed to __getitem__ if no __getslice__ is found or a three-limit extended slice form is used:

C:\code> c:\python27\python
>>> class Slicer:
        def __getitem__(self, index):     print index
        def __getslice__(self, i, j):     print i, j
        def __setslice__(self, i, j,seq): print i, j,seq

>>> Slicer()[1]        # Runs __getitem__ with int, like 3.X
1
>>> Slicer()[1:9]      # Runs __getslice__ if present, else __getitem__
1 9
>>> Slicer()[1:9:2]    # Runs __getitem__ with slice(), like 3.X!
slice(1, 9, 2)

These slice-specific methods are removed in 3.X, so even in 2.X you should generally use __getitem__ and __setitem__ instead and allow for both indexes and slice objects as arguments—both for forward compatibility, and to avoid having to handle two- and three-limit slices differently. In most classes, this works without any special code, because indexing methods can manually pass along the slice object in the square brackets of another index expression, as in the prior section’s example. See the section Membership: __contains__, __iter__, and __getitem__ for another example of slice interception at work.

But 3.X’s __index__ Is Not Indexing!

On a related note, don’t confuse the (perhaps unfortunately named) __index__ method in Python 3.X for index interception—this method returns an integer value for an instance when needed and is used by built-ins that convert to digit strings (and in retrospect, might have been better named __asindex__):

>>> class C:
        def __index__(self):
            return 255

>>> X = C()
>>> hex(X)               # Integer value
'0xff'
>>> bin(X)
'0b11111111'
>>> oct(X)
'0o377'

Although this method does not intercept instance indexing like __getitem__, it is also used in contexts that require an integer—including indexing:

>>> ('C' * 256)[255]
'C'
>>> ('C' * 256)[X]       # As index (not X[i])
'C'
>>> ('C' * 256)[X:]      # As index (not X[i:])
'C'

This method works the same way in Python 2.X, except that it is not called for the hex and oct built-in functions; use __hex__ and __oct__ in 2.X (only) instead to intercept these calls.