Giving Python slices a name

Python makes it easy for a developer to work with sequence types such as lists, strings, tuples, and others. This is especially true when extracting sub-sequences from a given sequence.

>>> vowels = ['A', 'E', 'I', 'O', 'U']
>>> vowels[1:3]
['E', 'I']
>>> vowels[3:5]
['O', 'U']
>>> vowels[-4:-2]
['E', 'I']

Out of bound indexes are gracefully handled:

>>> vowels[2:99]
['I', 'O', 'U']
>>> vowels[-5:2]
['A', 'E']

Omitted start/end indexes default to the beginning/end for the sequence, respectively:

>>> vowels[:2]
['A', 'E']
>>> vowels[-2:]
['O', 'U']

If given a step n, only every n-th item in the specified range is included in the result:

>>> vowels[::2]
['A', 'I', 'U']

Step can also be a negative number:

>>> vowels[4:1:-1]
['U', 'O', 'I']
Slice objects

When using the “extended indexing” syntax (made up the name) from above, what actually happens behind the scenes is that a slice() object is created and passed to the sequence object being sliced. The following two expressions are thus equivalent:

>>> vowels[4:2:-1]
['U', 'O']
>>> vowels[slice(4, 2, -1)]
['U', 'O']

This is great, because it allows us to assign descriptive names to slices, and possibly reusing them if the same sub-slice is used at more than a single place:

>>> FIRST_THREE = slice(0, 3)
>>> ODD_ITEMS = slice(1, None, 2)
>>> vowels[FIRST_THREE]
['A', 'E', 'I']
>>> 'abcdef'[FIRST_THREE]
>>> vowels[ODD_ITEMS]
['E', 'O']
>>> 'abcdef'[ODD_ITEMS]
Adding support for slicing to custom objects

It’s worth noting that object slicing is not something that is automatically given to us, Python merely allows us to implement support for it ourselves, if we want so.

When the square brackets notation ([]) is used, Python tries to invoke the __getitem__() magic method on the object, passing the given key to it as an argument. That method can be overridden to define custom indexing behavior.

As an example, let’s try to create a class whose instances can be queried for balance. Even if an instance itself does not contain anything, it will somehow caclulate the required amount out of thin air and return that made up number to us. We will call that class a Bank.

class Bank(object):
    """Can create money out of thin air."""

    def __getitem__(self, key):
        if not isinstance(key, (int, slice)):
            raise TypeError('Slice or integer index expected')

        if isinstance(key, int):
            return idx

        # key is a slice() instance
        start = key.start if isinstance(key.start, int) else 0
        stop = key.stop if isinstance(key.stop, int) else 0
        step = key.step if isinstance(key.step, int) else 1
        return sum(range(start, stop, step))

If we query a Bank instance (by indexing it) with a single integer, it will simply return us the amount equal to the given index. If queried by a range (slice), however, it will return the sum of all indices contained in it:1

>>> b = Bank()                                                                                                                                                                                                                                                                                                                                     
>>> b[7]                                                                                                                                                                                                                                                                                                                                           
>>> b[-3:0]                                                                                                                                                                                                                                                                                                                                      
>>> b[0:7:2]                                                                                                                                                                                                                                                                                                                                      
>>> bank[::]
>>> bank['':5:{}]

As the last example demonstrates, slices can contain just about any value, not just integers and None, thus the Bank class must check for these cases and use defaults if needed.

Just a word of caution – if sub-classing built-in types in Python 2, and want to implement custom slicing behavior, you need to override the deprecated __getslice__() method (documentation).

  1. Not saying that this is actually the best way to run a bank in real life, nor that (ab)using slices like this will make you popular with people using your sliceable class…