More on Generators with Python




Welcome to part 9 of the intermediate Python programming tutorial series. We're going to be revisiting the topic of generators again. In the previous generators tutorial, we simply wrote generator expressions, which is a "lazy" way to work with generators.

In many cases, you really don't need to actually write a generator over just writing a generator expression. It's also more Pythonic to go ahead and just write the generator expression since it's simpler. That said, you should still know how to write a generator, since it will give you a bit more insight into how generators work, and what iterable objects really are, since, despite looking a lot like list comprehension, generators are very unique in how they act. Also, you will want to be able to know a generator when you see it.

Generators don't return things, they yield things, in a stream. This can be done on iterables, which has been already illustrated, but also can be based purely in logic, whatever you want. For example:

def simple_gen():
    yield 'Oh'
    yield 'hello'
    yield 'there'

There's our generator, super basic. Now we can actually iterate over it:

for i in simple_gen():
    print(i)
Oh
hello
there

You can add logic to the yield statements, whatever you want. Let's consider a scenario where we want to find the combination to a lock, one of the ones where you spin the dials, and each dial has 0-9 on it, and we need to get the perfect combination to unlock the lock. How might a beginner approach this problem? I know how I would have started:

CORRECT_COMBO = (3, 6, 1)

for c1 in range(10):
    for c2 in range(10):
        for c3 in range(10):
            if (c1, c2, c3) == CORRECT_COMBO:
                print('Found the combo:{}'.format((c1, c2, c3)))

Sure enough, we do get the right answer:

Found the combo:(3, 6, 1)

So what's the problem? Well, even after we've found the combo, we continue to iterate through *all* of the combinations. Luckily for us, the calculation is super cheap, but this code doesn't scale at all. Even as beginners though, we know about the break statement. Can't we just call it like:

for c1 in range(10):
    for c2 in range(10):
        for c3 in range(10):
            if (c1, c2, c3) == CORRECT_COMBO:
                print('Found the combo:{}'.format((c1, c2, c3)))
                break

You can, but you're only saving yourself the processing of less than 10 out of a thousand operations. Hmm. Okay, we might be beginners, but we're still crafty. We'll have breaking logic for all of the lines. To do this, we're going to introduce a new var: found_combo, which we'll set to False to start, but then update to True if we find the combo. Then, we'll have an if statement to see if we found the combo at each step, and then break if that's the case!

found_combo = False
for c1 in range(10):
    if found_combo:
        break
    for c2 in range(10):
        if found_combo:
            break
        for c3 in range(10):
            if (c1, c2, c3) == CORRECT_COMBO:
                print('Found the combo:{}'.format((c1, c2, c3)))
                found_combo = True
                break

Great, we've done it! Is this Pythonic though? It meets PEP 8 standards...but this is one of the reasons why you need to take PEP 8 with a grain of salt, or, better put, think about PEP 8 only as you're writing code, but, after that, we need to assess whether or not it's *pythonic.* There's a better way. Since this is a generator tutorial, I suppose you might already know the answer is going to be a generator.

For the generator, we really need to just iterate through the combinations, the logic is nothing new or surprising:

def combo_gen():
    for c1 in range(10):
        for c2 in range(10):
            for c3 in range(10):
                yield (c1, c2, c3)

Then, to actually make use of it:

for (c1, c2, c3) in combo_gen():
    print(c1, c2, c3)
    if (c1, c2, c3) == CORRECT_COMBO:
        print('Found the combo:{}'.format((c1, c2, c3)))
        break

Once we break, the generator stream stops, and we're done.

This code is only 2 lines of actual code shorter (1 if we count white space), but it's easier to read and understand, and is certainly more beautiful.

Next up, let's talk about how to really amplify our Pythonic superpowers with multiprocessing.

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