While you can optimize the heck out of your Python code with generators and generator expressions I'm more interested in goofing around and solving classic programming questions with the yield statement.
note: For this article, since it's easier to explain things as they happen, I'll be including a lot of inline comments.
Let's start with a simple function that returns a sequence of some of my favorite values:
# yielding.py def pydanny_selected_numbers(): # If you multiple 9 by any other number you can easily play with # numbers to get back to 9. # Ex: 2 * 9 = 18. 1 + 8 = 9 # Ex: 15 * 9 = 135. 1 + 3 + 5 = 9 # See https://en.wikipedia.org/wiki/Digital_root yield 9 # A pretty prime. yield 31 # What's 6 * 7? yield 42 # The string representation of my first date with Audrey Roy yield "2010/02/20"
note: When a function uses the yield keyword it's now called a generator.
Let's do a test drive in the REPL:
>>> from yielding import pydanny_selected_numbers # import ye aulde code >>> pydanny_selected_numbers() # create the iterator object <generator object pydanny_selected_numbers at 0x1038a03c0> >>> for i in pydanny_selected_numbers(): # iterate through the iterator ... print(i) ... 9 31 42 "2010/02/20" >>> iterator = pydanny_selected_numbers() # create the iterator object >>> for i in iterator: # iterate through the iterator object ... print(i) ... 9 31 42 "2010/02/20"
Of course, if you know anything about generator expressions, you know I could do this more tersely with the following:
>>> iterator = (x for x in [9, 31, 42, "2010/02/20"]) >>> for i in iterator: ... print(i) ... 9 31 42 "2010/02/20"
While that is more terse, it doesn't give us the amount of control we get by defining our own generator function. For example, what if I want to present the Fibonacci sequence in a loop rather than with recursion?
# fun.py def fibonacci(max): result = 0 base = 1 while result <= max: # This yield statement is where the execution leaves the function. yield result # This is where the execution comes back into the function. This is # just whitespace, but that it came back while preserving the state # of the function is pretty awesome. # Fibonacci code to increase the number according to # https://en.wikipedia.org/wiki/Fibonacci_number n = result + base result = base base = n if __name__ == "__main__": for x in fibonacci(144): print(x)
Let's try this out in the REPL:
>>> from fun import fibonacci >>> fibonacci(10) <generator object fibonacci at 0x10d49e460> >>> for x in fibonacci(10): ... print(x) 0 1 1 2 3 5 8 >>> iterator = fibonacci(5) >>> iterator <generator object fibonacci at 0x10d63c550> >>> iterator.next() 0 >>> iterator.next() 1
What's nice about this is so much more than fibonacci logic in a generator function. Instead, imagine instead of a lightweight calculation I had done something performance intensive. By using generator expressions I can readily control the execution calls with the iterator object's next() method, saving processor cycles.
I admit it. Like many Python developers, I find using tools like yields and generators to optimize the heck out of performance intensive things a lot of fun.
If you are like me and like this sort of stuff, I recommend the following resources:
- Matt Harrison's Treading on Python Volume 2: Intermediate Python
- Jeff Knupp's Improve Your Python: 'yield' and Generators Explained
In the next article I'll demonstrate how to use the yield statement to create context managers.
Update: Nicholas Tollervey pointed me at wikipedia's Digital root article, so I added it to the comments of the first code sample.
Update: Oddthinking pointed out that I forgot a print statement. In the REPL it's not really needed, but if this is translated to a script then it's necessary.