File: gendemo.py
"""
================================================================================
Demonstrate the many mutations of generators though the years...
(Copyright M. Lutz 2015)
Contents:
1) In 2.3+: original model - yield
2) In 2.5+: sends (plus throws, closes)
3) In 3.3+: returns and subgenerators
4) In 3.5+: asynch/await (plus asyncio in 3.4)
This is a supplement to the book "Learning Python". Its first few sections'
code runs under both Python 2.X and 3.X, but later code requires recent 3.X.
Generators and coroutines in Python:
-Have seen a volatile and even tortured evolution, that likely qualifies
as thrashing
-Have advanced and obscure usage modes that are not widely used by most
Python programmers
-In advanced roles, are more in the realm of application programming
than language, as they entail concurrent programming techniques.
However, they have recently spawned new syntax that can regrettably make
them a required topic for all users and learners of the core language.
================================================================================
"""
#
# See also: expression equivalent, in 2.4+
#
>>> for x in (i ** 2 for i in range(4)): # Automatically yields values
... print(x) # But defs can use full statements
...
0
1
4
9
>>>
#===============================================================================
# 1) In 2.3+: original model - yield
#
# A def with a "yield" statement automatically supports the iteration
# protocol: yielded values are returned for next(). This suffices for
# simple task switching: see http://learning-python.com/books/async1.py.
#===============================================================================
>>> def a(N): # YIELD VALS ONLY
... for i in range(N): # Suspend and resume just after yield
... yield i ** 2 # Result of caller's next() or send()
...
>>> G = a(4) # Manual iteration: iteration protocol
>>> next(G) # Yielded values recieved at top by caller
0
>>> next(G)
1
>>> next(G)
4
>>> next(G)
9
>>> next(G)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> for top in a(4): print(top) # Automatic iteration: all contexts
... # Calls next() * N, catches exception
0
1
4
9
>>>
#
# Example: classic producer/consumer model
# See also task switcher link above for a more general model
# Threads + thread queues achieve similar effect without manual yields
#
>>> queue = [] # Shared object and global name
>>> def produce(N):
... for i in range(N):
... queue.append(i ** 2)
... yield # Suspend, yield control to consume()
...
>>> def consume(N):
... for i in produce(N): # Start or resume produce() on each loop
... res = queue.pop()
... print(res)
...
>>> consume(4)
0
1
4
9
>>>
#===============================================================================
# 2) In 2.5+: sends (plus throws and closes, not shown)
#
# The caller can resume a generator and pass it a value via G.send(X):
# X becomes the result of the "yield" expression inside the generator.
#===============================================================================
>>> def a(N): # YIELD VALS + GET SENT VALS
... for i in range(N):
... X = yield i ** 2 # X is result of caller's send()
... print('gen:', X) # Display X after resumed
...
#
# Iterate manually
#
>>> G = a(4) # Create generator
>>> G.send(77) # Must start with next(), not send()
TypeError: can't send non-None value to a just-started generator
>>> top = next(G) # Start generator: to first yield
>>> top # Yielded iteration value in caller
0
>>> top = G.send(77) # 77 sent to suspended yield expression
gen: 77 # Sent value printed in generator
>>> top # Yielded iteration value in caller
1
>>> top = G.send(88) # 88 sent to suspended yield expression
gen: 88 # Sent value printed in generator
>>> top # Yielded iteration value in caller
4
>>> top = next(G)
gen: None # No value (default None) sent by next()
>>> top # Final yielded iteration value
9
>>> G.send(99) # Resume a(), a() exits, no new value
gen: 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
#
# Iterate with loops
#
>>> for x in a(4): print('top:', x) # next() sends default None to a()
...
top: 0
gen: None
top: 1
gen: None
top: 4
gen: None
top: 9
gen: None
>>> G = a(4) # Send explicit values to a()
>>> v = next(G)
>>> print(v)
0
>>> while True:
... v = G.send('Spam' * (v+1))
... print('top:', v)
...
gen: Spam
top: 1
gen: SpamSpam
top: 4
gen: SpamSpamSpamSpamSpam
top: 9
gen: SpamSpamSpamSpamSpamSpamSpamSpamSpamSpam
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
StopIteration
>>>
#
# Example: using sent value inside a generator
# Yielded values can influence the caller's next action
# Sent values can influence the generator's next iteration
#
>>> def a(N):
... X = True
... for i in range(N):
... if X:
... result = i ** 2
... else:
... result = 1 / float(i)
... X = yield result
...
>>> G = a(4)
>>> next(G) # Generator runs up to first yield and suspends
0
>>> G.send(False) # Suspended yield resumed, returns sent value
1.0
>>> G.send(True) # Sent value always impacts _next_ iteration
4
>>> G.send(False)
0.3333333333333333
>>> G.send(True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
#===============================================================================
# 3) In 3.3+: returns and subgenerators
#
# A "return X" is allowed in generators: X is attached to the StopIteration
# exception and becomes the result of a "yield from" subgenerator expression,
# which also propagates sent and yielded values to and from subgenerators.
#===============================================================================
C:\Code> py -3.5 # 3.3 and later
>>> def a(N): # YIELD VALS + RETURN RESULT
... for i in range(N):
... yield i ** 2 # yield: result of iterations
... return 'SPAM' # return: result of "yield from" expression
...
>>> def b(N):
... print('before...')
... res = yield from a(N) # Exports subgenerator iteration results
... print('after...', res) # res is "return" value (or None by default)
...
>>> for x in a(4): print('top:', x) # Iterate through subgenerator directly
...
top: 0
top: 1
top: 4
top: 9
>>>
>>> for x in b(4): print('top:', x) # Iterate through subgenerator indirectly
...
before...
top: 0
top: 1
top: 4
top: 9
after... SPAM
>>>
>>> G = a(4)
>>> try:
... while True: next(G)
... except StopIteration as E: # Catch return value in exception handler
... print(E.value)
...
0
1
4
9
SPAM
>>>
>>> b(4) # Generators are nothing till started
<generator object b at 0x00...B65990>
>>> def a(N):
... for i in range(N):
... yield i ** 2 # Returns None by default (as usual)
...
>>> for x in b(4): print('top:', x) # b() delegates to the new a()
...
before...
top: 0
top: 1
top: 4
top: 9
after... None
>>>
#
# DRUMROLL PLEASE... (all behavior combined)
#
>>> def a(N): # YIELD VALS + GET SENT VALS + RETURN RESULT
... for i in range(N):
... X = yield i ** 2 # X is result of highest send()
... print('gen:', X) # Display X after resumed
... return 'SPAM' # return: result of "yield from" expression
...
>>> def b(N):
... print('before...')
... res = yield from a(N) # Exports subgenerator iteration results
... print('after...', res) # res is "return" value (or None by default)
...
#
# Iterate manually
#
>>> G = b(4) # Create generator
>>> next(G) # Start: to first yield in b() then a(), 0 passed up to top
before...
0
>>> G.send(77) # 77 sent to b() then a(), a() yields 1 to b() then top
gen: 77
1
>>> G.send(88) # ditto, but sends 88 and receives 4
gen: 88
4
>>> next(G) # next() sends default None, a() yields 9 to b() then top
gen: None
9
>>> G.send(99) # Send 99 to a(), a() exits + returns 'SPAM' to b(), b() exits
gen: 99
after... SPAM
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
#
# Iterate with loops
#
>>> for x in b(4): print('top:', x) # next() sends default None to a() via b()
...
before...
top: 0
gen: None
top: 1
gen: None
top: 4
gen: None
top: 9
gen: None
after... SPAM
>>> G = b(4) # Send explicit values to a() via b()
>>> v = next(G)
before...
>>> v
0
>>> while True:
... v = G.send('Eggs' * (v+1))
... print('top:', v)
...
gen: Eggs
top: 1
gen: EggsEggs
top: 4
gen: EggsEggsEggsEggsEggs
top: 9
gen: EggsEggsEggsEggsEggsEggsEggsEggsEggsEggs
after... SPAM
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
StopIteration
>>>
#
# Example: using sent values inside a subgenerator
# Both "yield" and "yield from" require "()" unless alone on right of "="
#
>>> def a(N):
... sum = 0
... for i in range(N):
... sum += i + (yield i) # Add on i plus sent value
... return sum # (0+1+2+3) + (10+20+30+40)
...
>>> def b(N):
... res = yield from a(N) # Get subgenerator return value
... return float(res) # Return float conversion here
...
>>> G = b(4)
>>> next(G) # Run to first yield in a()
0
>>> G.send(10) # Send 10 to suspended yield
1
>>> G.send(20)
2
>>> G.send(30)
3
>>> G.send(40) # Send 40, a() exits, b() exits
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: 106.0
>>>
>>> ^Z
C:\Code> py -3.2 # Return allowed in generators in 3.3+ only
>>> def a():
... for i in range(4):
... yield i ** 2
... return 'SPAM'
...
SyntaxError: 'return' with argument inside generator
#===============================================================================
# 4) In 3.5+: asynch/await (plus asyncio in 3.4)
#
# New "async" and "await" core language syntax attempts to distinguish some
# coroutine roles, by adding an entirely new and incompatible model.
#
# This new model in 3.5:
#
# -Builds on ideas in the new "asyncio" standard library module in 3.4
# -Which builds on the subgenerators added in 3.3
# -Which builds on the extended generators of 2.5
# -Which assumes the original model of 2.3 (and 2.4)
#
# For details, see:
#
# -The PEP for change 0492
# -Python 3.5's documentation
# -http://learning-python.com/books/python-changes-2014-plus.html#coroutines
# -Or your local priest...
#===============================================================================
# This space intentionally left blank