67

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!

xster
  • 6,269
  • 9
  • 55
  • 60
  • p.s. the data contains a list attribute of course and I can do class().list[1] but can I do class()[1]? – xster Mar 19 '11 at 02:43
  • 1
    Irrelevant but if you are using `class` like that, you are bound to get errors. – 0xc0de May 31 '15 at 11:44

2 Answers2

105

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!

Rick
  • 43,029
  • 15
  • 76
  • 119
  • 4
    You'll find that some Pythonistas on the Internetz don't seem to like the idea of including abstract classes and methods as part the language (to summarize/parody their reasoning a bit, Python is supposed to be "NOT Java!"). However, they seem to be extremely useful, so I'm glad they're there. – Rick Jan 06 '15 at 17:21
  • Also: be sure to take a look at the [`abc` module as well](https://docs.python.org/3/library/abc.html), which allows you to create your own ABCs. – Rick Jan 06 '15 at 20:38
  • This seems to be specific to Python 3. – new name Feb 17 '15 at 16:05
  • Might be. I've only ever used 3. – Rick Feb 17 '15 at 17:03
  • 1
    Thanks for pointing this out, I was just looking up to to implement indexing, but I guess I'll go ahead and use an ABC instead. Very handy! – flotzilla Dec 25 '15 at 12:59
  • 1
    The Abstract Base classes were moved from collections to collections.abc in python 3. In python 2 just use `from collections import Sequence` – DylanYoung Feb 24 '18 at 19:44
70

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

FLAK-ZOSO
  • 3,873
  • 4
  • 8
  • 28
Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358