Special Methods, OOP, and Iteration Python Tutorial




Welcome to part 21 of the intermediate Python programming tutorial series. In this tutorial, we're going to take a unique opportunity to mesh OOP, iterables, iterators, special methods, AND generators. It's an exciting day.

There are all sorts of special methods that we can add to our classes, but one common one might be to either add iteration functionality, or even to modify how something is iterated over. Let's revisit iterables.

When we do something like:

x = range(5)

What is x? Is it an iterator? Is it iterable? Is it a Generator? ...is it a range object?

Is there a difference between x = range(5) and x = (i for i in range(5)) in what x is? Your homework is to answer that question.

Let's say:

x = (i for i in range(5))

Now, how do we iterate over this?

for i in x:
    print(i)

Output:

0
1
2
3
4

What happens here, I mean *really*? As you iterate over an iterable, which is a Python object, you're actually calling the next method to run. If this is news to you, check this out:

x = (i for i in range(5))
next(x)
next(x)
for i in x:
    print(i)

Output:

2
3
4

With next, we move the iterator (think of it like a "selector") explicitly, before iterating over whatever was left. Now comes the OOP part. Python is such a high level language, and there's so much going on, that even the most basic of scripts is actually pretty complex Object Oriented Programming, even when the author has no idea. In our case, it wouldn't appear to us that we've got any OOP going on here, but we do. Everything's an object. That's why Python's Pickle is so darned useful. Rather than using next(), we can also actually just use the raw method, a special method: __next__.

x = (i for i in range(5))
x.__next__()
x.__next__()
for i in x:
    print(i)

Output:

2
3
4

Works the same way. Jeez x, what other methods are you hiding from us?! Given that __next__ exists, we can rest assured __iter__ also exists. That's it x, you're getting searched. We'll use Python's built-in dir function to return the attributes of this x object:

>>> print(dir(x))
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

!!!!!!!!

Look at all of those dunders that x has been hiding from us. Never trust an x. Checking range is a similar story:

>>> print(dir(range))
['__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index', 'start', 'step', 'stop']

Sheesh. We can trust no one. We need to handle this iteration stuff on our own, clearly.

Let's make a new class, called range_examp, and build the __init__ method:

class range_examp:
    
    def __init__(self, end, step=1):
        self.current = 0
        self.end = end
        self.step = step

Any good range function is going to need to know where it is and where to head next. Other than that, it doesn't need much else, but we'll also give it a stopping point. Now, let's do the entirely challenging task of writing the __iter__ method...

    def __iter__(self):
        return self

Whew, that was hard. Now we need to write the dunder next method.

    def __next__(self):
        if self.current >= self.end:
            raise StopIteration()
        else:
            return_val = self.current
            self.current += self.step
            return return_val

Simple enough above. If the current value is at or beyond the end value, then we're done, and we raise a StopIteration(). Otherwise, we set the return value to be the current value, we add the step, and return the current value. Now, we can do:

for i in range_examp(5):
    print(i)
0
1
2
3
4

Fancy! We can also illustrate the use of __next__:

i = range_examp(5)

i.__next__()
i.__next__()

for j in i:
    print(j)

Output:

2
3
4

Of course, we still have a lot of hidden attributes to even this new class that we just made...

>>> print(dir(range_examp))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

...not to mention that StopIteration, given its TitleCasing is obviously also a class, with a bunch of methods and attributes of its own. Alright, that's it, let's just use a generator for this range stuff:

def range_gen(end):
    current = 0
    while current < end:
        yield current
        current += 1

Perfect, good ol' functions. No classes here. We can do something like:

for i in range_gen(5):
    print(i)

Output:

0
1
2
3
4

But then again...

>>> print(dir(range_gen(5)))
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

There's no getting away from Object Oriented Programming in Python 3!

The next tutorial:





  • Intermediate Python Programming introduction
  • String Concatenation and Formatting Intermediate Python Tutorial part 2
  • Argparse for CLI Intermediate Python Tutorial part 3
  • List comprehension and generator expressions Intermediate Python Tutorial part 4
  • More on list comprehension and generators Intermediate Python Tutorial part 5
  • Timeit Module Intermediate Python Tutorial part 6
  • Enumerate Intermediate Python Tutorial part 7
  • Python's Zip function
  • More on Generators with Python
  • Multiprocessing with Python intro
  • Getting Values from Multiprocessing Processes
  • Multiprocessing Spider Example
  • Introduction to Object Oriented Programming
  • Creating Environment for our Object with PyGame
  • Many Blobs - Object Oriented Programming
  • Blob Class and Modularity - Object Oriented Programming
  • Inheritance - Object Oriented Programming
  • Decorators in Python Tutorial
  • Operator Overloading Python Tutorial
  • Detecting Collisions in our Game Python Tutorial
  • Special Methods, OOP, and Iteration Python Tutorial
  • Logging Python Tutorial
  • Headless Error Handling Python Tutorial
  • __str__ and __repr_ in Python 3
  • Args and Kwargs
  • Asyncio Basics - Asynchronous programming with coroutines