Python class accessible by iterator and index

Question:

Might be a n00b question, but I currently have a class that implements an iterator so I can do something like

for i in class():

but I want to be able to access the class by index as well like

class()[1]

How can I do that?

Thanks!

Asked By: xster

||

Answers:

Implement both __iter__() and __getitem__() et alia methods.

The current accepted answer from @Ignacio Vazquez-Abrams is sufficient. However, others interested in this question may want to consider inheriting their class from an abstract base class (ABC) (such as those found in the standard module collections.abc). This does a number of things (there are probably others as well):

  • ensures that all of the methods you need to treat your object “like a ____” are there
  • it is self-documenting, in that someone reading your code is able to instantly know that you intend your object to “act like a ____”.
  • allows isinstance(myobject,SomeABC) to work correctly.
  • often provides methods auto-magically so we don’t have to define them ourselves

(Note that, in addition to the above, creating your own ABC can allow you to test for the presence of a specific method or set of methods in any object, and based on this to declare that object to be a subclass of the ABC, even if the object does not inherit from the ABCdirectly. See this answer for more information.)


Example: implement a read-only, list-like class using ABC

Now as an example, let’s choose and implement an ABC for the class in the original question. There are two requirements:

  1. the class is iterable
  2. access the class by index

Obviously, this class is going to be some kind of collection. So what we will do is look at our menu of collection ABC’s to find the appropriate ABC (note that there are also numeric ABCs). The appropriate ABC is dependent upon which abstract methods we wish to use in our class.

We see that an Iterable is what we are after if we want to use the method __iter__(), which is what we need in order to do things like for o in myobject:. However, an Iterable does not include the method __getitem__(), which is what we need in order to do things like myobject[i]. So we’ll need to use a different ABC.

On down the collections.abc menu of abstract base classes, we see that a Sequence is the simplest ABC to offer the functionality we require. And – would you look at that – we get Iterable functionality as a mixin method – which means we don’t have to define it ourselves – for free! We also get __contains__, __reversed__, index, and count. Which, if you think about it, are all things that should be included in any indexed object. If you had forgotten to include them, users of your code (including, potentially, yourself!) might get pretty annoyed (I know I would).

However, there is a second ABC that also offers this combination of functionality (iterable, and accessible by []): a Mapping. Which one do we want to use?

We recall that the requirement is to be able to access the object by index (like a list or a tuple), i.e. not by key (like a dict). Therefore, we select Sequence instead of Mapping.


Sidebar: It’s important to note that a Sequence is read-only (as is a Mapping), so it will not allow us to do things like myobject[i] = value, or random.shuffle(myobject). If we want to be able do things like that, we need to continue down the menu of ABCs and use a MutableSequence (or a MutableMapping), which will require implementing several additional methods.


Example Code

Now we are able to make our class. We define it, and have it inherit from Sequence.

from collections.abc import Sequence

class MyClass(Sequence):
    pass

If we try to use it, the interpreter will tell us which methods we need to implement before it can be used (note that the methods are also listed on the Python docs page):

>>> myobject = MyClass()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class MyClass with abstract methods __getitem__, __len__

This tells us that if we go ahead and implement __getitem__ and __len__, we’ll be able to use our new class. We might do it like this in Python 3:

from collections.abc import Sequence

class MyClass(Sequence):
    def __init__(self,L):
        self.L = L
        super().__init__()
    def __getitem__(self, i):
        return self.L[i]
    def __len__(self):
        return len(self.L)

# Let's test it:
myobject = MyClass([1,2,3])
try:
    for idx,_ in enumerate(myobject):
        print(myobject[idx])
except Exception:
    print("Gah! No good!")
    raise
# No Errors!

It works!

Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.