Iterating over dictionary using __getitem__ in python

Viewed 1429

I've implemented a python class to generate data as follows:

class Array:
    def __init__(self):
        self.arr = [1,2,3,4,5,6,7,8]

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

a = Array()
for i in a:
    print(i, end = " ")

It runs as expected and I get following

1 2 3 4 5 6 7 8

However, I want to do the same thing for dictionary. Why am I not able to iterate over dictionary like this?

class Dictionary:
    def __init__(self):
        self.dictionary = {'a' : 1, 'b' : 2, 'c': 3}

    def __getitem__(self,key):
        return self.dictionary[key]
d = Dictionary()
for key in d:
    print(key, d[key], end=" ")

I expect following output

a 1 b 2 c 3

But when I run above code, I get following error :

Traceback (most recent call last):

  File "<ipython-input-33-4547779db7ec>", line 8, in <module>
    for key in d:

  File "<ipython-input-33-4547779db7ec>", line 6, in __getitem__
    return self.dictionary[key]

KeyError: 0

We can iterate over normal dictionary like this : for key in d. This iterates over all the keys, is it not possible to iterate like this using __getitem__()?

2 Answers

A for loop works with iterators, objects you can pass to next. An object is an iterator if it has a __next__ method.

Neither of your classes does, so Python will first pass your object to iter to get an iterator. The first thing iter tries to do is call the object's __iter__ method.

Neither of your classes defines __iter__, either, so iter next checks if its object defines __getitem__. Both of your classes do, so iter returns an object of type iterator, whose __next__ method can be imagined to be something like

def __next__(self):
    try:
        rv = self.thing.__getitem__(self.i)
    except IndexError:
        raise StopIteration
    self.i += 1
    return rv

(The iterator holds a reference to the thing which defined __getitem__, as well as the value of i to track state between calls to __next__. i is presumed to be initialized to 0.)

For Array, this works, because it has integer indices. For Dictionary, though, 0 is not a key, and instead of raising an IndexError, you get a KeyError with the __next__ method does not catch.

(This is alluded to in the documentation for __getitem__:

Note for loops expect that an IndexError will be raised for illegal indexes to allow proper detection of the end of the sequence.

)

To make your Dictionary class iterable, define __iter__

class Dictionary:
    def __init__(self):
        self.dictionary = {'a' : 1, 'b' : 2, 'c': 3}

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

    def __iter__(self):
        return iter(self.dictionary)

dict.__iter__ returns a value of type dict_keyiterator, which is the thing that yields the dict's keys, which you can use with Dictionary.__getitem__.

Adding the following method to your Dictionary class is sufficient to get it to work:

def __iter__(self):
    return iter(self.dictionary)

Important note: When creating custom classes that are sequences or mappings, you should implement all of the relevant methods. Otherwise, your code (e.g., x in y, for x in y etc) will either be inefficient or broken as you saw in the case of your Dictionary class.

See for more details: https://docs.python.org/3/reference/datamodel.html?emulating-container-types#emulating-container-types.

Related