Python __index__ special method

wim picture wim · Dec 12, 2014 · Viewed 20.3k times · Source
>>> class Thing(object):
...     def __index__(self):
...         return 1
... 
>>> thing = Thing()
>>> list_ = ['abc', 'def', 'ghi']
>>> list_[thing]
'def'
>>> dict_ = {1: 'potato'}
>>> dict_[thing]
# KeyError

How does thing know to represent itself as 1 when accessed by a list, but not by a dict? Don't both magic methods go through __getitem__? The usage shown for lists could go through __int__ instead so what is the raison d'être for __index__ anyway?

Answer

user2555451 picture user2555451 · Dec 12, 2014

@BenoîtLatinier was correct when he said:

Dict and List does not implement __getitem__ the same way.

However, I'd like to add a little more information. According to the documentation:

object.__index__(self)

Called to implement operator.index(), and whenever Python needs to losslessly convert the numeric object to an integer object (such as in slicing, or in the built-in bin(), hex() and oct() functions). Presence of this method indicates that the numeric object is an integer type. Must return an integer.

The part I bolded is important. Indexing and slicing on a list are both handled by the same method (namely, __getitem__). So, if Thing.__index__ is called for slicing, it will likewise be called for indexing since we are using the same method. This means that:

list_[thing]

is roughly equivalent to:

list_[thing.__index__()]

For the dictionary however, Thing.__index__ is not being called (there is no reason to call it since you cannot slice a dictionary). Instead, doing dict_[thing] is telling Python to find a key in the dictionary that is the thing instance itself. Since this doesn't exist, a KeyError is raised.

Perhaps a demonstration will be helpful:

>>> class Thing(object):
...     def __index__(self):
...         print '__index__ called!'
...         return 1
...
>>> thing = Thing()
>>> list_ = ['abc', 'def', 'ghi']
>>> list_[thing]  # __index__ is called
__index__ called!
'def'
>>>
>>> dict_ = {1: 'potato'}
>>> dict_[thing]  # __index__ is not called
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: <__main__.Thing object at 0x01ACFC70>
>>>
>>> dict_ = {thing: 'potato'} # Works if thing is a key
>>> dict_[thing]
'potato'
>>>

As for why __index__ exists in the first place, the reason is thoroughly listed in PEP 0375. I won't repeat all of it here, but basically it is so that you can allow arbitrary objects to serve as integers, which is needed in slicing as well as a few other applications.