Python generators: understanding the order of execution - python

21 def power(values):
22 print "power", values
23 for value in values:
24 print 'powering %s' % value
25 yield value
26
27 def adder(values):
28 print "adder", values
29 for value in values:
30 print 'adding to %s' % value
31 if value % 2 ==0:
32 yield value + 3
33 else:
34 yield value + 2
35
36 elements = [1, 4, 7, 9, 12, 19]
37 res = adder(power(elements))
38 print res.next()
39 print res.next()
40 print res.next()
41 print res.next()
OUTPUT:
adder <generator object power at 0x7fb6b9ee7910> <--- is this the stdout flush matter?
power [1, 4, 7, 9, 12, 19]
powering 1
adding to 1
3
powering 4
adding to 4
7
powering 7
adding to 7
9
powering 9
adding to 9
11
I am trying to understand the above code.
1) why did adder got printed before power [1, 4, 7, 9, 12, 19]?
2) adder is not iterating through elements but iterating through power generator, correct?
3) confirm my thought on (1). so adder gets called first then in for value in values for adder, it's consulting power generator and hence, powering print line gets triggered and then adder print line gets triggered?
4) If so, why doesnt the print statement power [1, 4, 7, 9, 12, 19] gets called along with the powering <$> print statement every time?

power has a yield return, which makes it a generator. The code in the function itself is only execute when next() is called on it.
correct. adder relies on the generator, and has no information on the data being iterated. (such as the size)
correct again
yield is a particular instruction. It does not return from the generator function (power). Instead, it provides one value, and suspend its execution until the next call to next(). At this point, the execution is resumed at the same point, which is inside the loop.
Edit
Illustration of the yield stopping point:
def gene():
print 'first!'
yield 1
print 'second!'
yield 2
g = gene()
g.next()
# first!
# 1
g.next()
# second!
# 2
As you can see, the generator is interrupted exactly after the yield instruction, ready to execute the next one

Generator functions don't begin execution until the first call to __next__ on the generator. For example:
>>> def gen():
... print 'starting execution'
... for i in range(10): yield i
...
>>> itr = gen()
>>> next(itr)
starting execution
0
So to answer you first question, "why did adder got printed before power", when you do adder(power(elements)) the generator power(elements) is created first, but the execution of that generator will not begin until the first iteration of for value in values within adder().

Code inside a generator is not run until next is called:
def gen():
print "called"
yield 3.14
g = gen() # nothing is printed
g.next() # print "called" and return 3.14
The for loop is doing the job of calling next here, in your code this is happening after the adder is printed:
g = gen()
print 'adder'  # prints adder
for i in g: # prints called (as inside generator, *before* yields)
print i # prints 3.14

Related

How can I restart a list once StopIteration exception is thrown or a certain point in list is reached? [duplicate]

It's my understanding that using a Generator is the best way to achieve something like this, but I'm open to suggestions.
Specifically, one use case is this: I'd like to print some items alongside another list, of an arbitrary length, truncating the initial iterator as necessary.
Here is working python code that demonstrates the exact example behavior I desire:
def loop_list(iterable):
"""
Return a Generator that will infinitely repeat the given iterable.
>>> l = loop_list(['sam', 'max'])
>>> for i in range(1, 11):
... print i, l.next()
...
1 sam
2 max
3 sam
4 max
5 sam
6 max
7 sam
8 max
9 sam
10 max
>>> l = loop_list(['sam', 'max'])
>>> for i in range(1, 2):
... print i, l.next()
...
1 sam
"""
iterable = tuple(iterable)
l = len(iterable)
num = 0
while num < l:
yield iterable[num]
num += 1
if num >= l:
num = 0
The Problem / My Question
As you may have noticed, this only works on lists/tuples/iterables that implement __getitem__ (if I'm not mistaken). Ideally, I'd like to be able to pass any iterable, and receive a generator that can properly loop over it's content.
If there's a better way to do something like this without a generator, I'm fine with that as well.
You can use itertools.cycle (source included on linked page).
import itertools
a = [1, 2, 3]
for element in itertools.cycle(a):
print element
# -> 1 2 3 1 2 3 1 2 3 1 2 3 ...
Try this-
L = [10,20,30,40]
def gentr_fn(alist):
while 1:
for j in alist:
yield j
a = gentr_fn(L)
print a.next()
print a.next()
print a.next()
print a.next()
print a.next()
print a.next()
print a.next()
>>gentr_fn(x,y)
10 20 30 40 10 20 30 ...
You can use module to keep it simple.
Just make sure not to start iterating from 0.
my_list = ['sam', 'max']
for i in range(1, 100):
print(my_list[(i % len(my_list))-1])
You can iterate over a list, appending an item to it:
somelist = [item1, item2, item3]
for item in somelist:
somelist.append(item)
do_something()

Python Loop Equivalent Excel Offset equivalent?

I am new to programming in Python and am trying to write a For/While loop that returns a value relative to another in a list based on the current value in the loop. The closest analog from excel I can think of is the OFFSET function, but my searches have failed to come up with an answer. Also, I believe VBA in excel allows you to use relative cells instead of absolute. Maybe this is a better analog.
As an example:
Suppose I have a simple list abc = [2,5,21,54,23,12,23] and I want to create a variable that is the current value in the loop divided by the prior value. So in this instance 5 would be divided by 2, 21 divided by 5, etc.
An example of the code would like something like:
for val in abc:
xyz = val/val[-1]
print(xyz)
I understand I would need to write an exception for the first iteration and the syntax is incorrect, but just trying to convey my message.
My search found that pandas has an ability to do this, but I was wanting to know if it is possible to do without pandas. Would appreciate any input.
You can't just loop by value since the value has no context of its position in the list. One way is to loop by index:
# start from 1, not 0
for i in range(1, len(abc)):
xyz = abc[i] / abc[i - 1]
print(xyz)
Or, zip the list with its tail:
for prev, cur in zip(abc, abc[1:]):
xyz = cur / prev
print(xyz)
For intuition on why this works:
prev
v
abc -> 1 2 3 4 5 6 7
abc[1:] -> 2 3 4 5 6 7
^
cur
You can use enumerate to do this:
abc = [2,5,21,54,23,12,23]
for index, val in enumerate(abc):
print("VALUE: {}, PREVIOUS: {}, RESULT: {}".format(val, abc[index-1], val/abc[index-1]))
However, there is a gotcha here. Python can have negative indexes. Where it wraps around the the other side of the list. If you do the above, the first time through the loop you will see it do this:
VALUE: 2, PREVIOUS: 23, RESULT: 0.08695652173913043
So, to protect against that, you could do this:
abc = [2,5,21,54,23,12,23]
for index, val in enumerate(abc):
if index > 0:
print("VALUE: {}, PREVIOUS: {}, RESULT: {}".format(val, abc[index-1], val/abc[index-1]))
else:
print("First Value")
Resulting in this output:
First Value
VALUE: 5, PREVIOUS: 2, RESULT: 2.5
VALUE: 21, PREVIOUS: 5, RESULT: 4.2
VALUE: 54, PREVIOUS: 21, RESULT: 2.5714285714285716
VALUE: 23, PREVIOUS: 54, RESULT: 0.42592592592592593
VALUE: 12, PREVIOUS: 23, RESULT: 0.5217391304347826
VALUE: 23, PREVIOUS: 12, RESULT: 1.9166666666666667
Pythonic style:
abc = [2,5,21,54,23,12,23]
gen = (abc[i + 1] / v for i, v in enumerate(abc) if i + 1 < len(abc))
for i in gen:
print(i)
Result:
2.5
4.2
2.5714285714285716
0.42592592592592593
0.5217391304347826
1.9166666666666667

Python: yield and yield assignment

How does this code, involving assignment and the yield operator, work? The results are rather confounding.
def test1(x):
for i in x:
_ = yield i
yield _
def test2(x):
for i in x:
_ = yield i
r1 = test1([1,2,3])
r2 = test2([1,2,3])
print list(r1)
print list(r2)
Output:
[1, None, 2, None, 3, None]
[1, 2, 3]
The assignment syntax ("yield expression") allows you to treat the generator as a rudimentary coroutine.
First proposed in PEP 342 and documented here: https://docs.python.org/2/reference/expressions.html#yield-expressions
The client code that is working with the generator can communicate data back into the generator using its send() method. That data is accessible via the assignment syntax.
send() will also iterate - so it actually includes a next() call.
Using your example, this is what it would be like to use the couroutine functionality:
>>> def test1(x):
... for i in x:
... _ = yield i
... yield _
...
>>> l = [1,2,3]
>>> gen_instance = test1(l)
>>> #First send has to be a None
>>> print gen_instance.send(None)
1
>>> print gen_instance.send("A")
A
>>> print gen_instance.send("B")
2
>>> print gen_instance.send("C")
C
>>> print gen_instance.send("D")
3
>>> print gen_instance.send("E")
E
>>> print gen_instance.send("F")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Note that some of the sends are lost because of the second yield in each loop iteration that doesn't capture the sent data.
EDIT:
Forgot to explain the Nones yielded in your example.
From https://docs.python.org/2/reference/expressions.html#generator.next:
When a generator function is resumed with a next() method, the current
yield expression always evaluates to None.
next() is used when using the iteration syntax.
_ = yield i
yield _
First it yields the value referenced by i, e.g. 1. Then it yields the value returned by the yield operation, which is None. It does this on each iteration of the loop.
for i in x:
_ = yield i
This simply yields the value referenced by i, e.g. 1, then proceeds to the next iteration of the loop, producing 2, then 3.
Unlike return, the yield keyword can be used in an expression:
x = return 0 # SyntaxError
x = yield 0 # perfectly fine
Now, when the interpreter sees a yield, it will generate the indicated value. However, when it does so, that operation returns the value None, just like mylist.append(0) or print('hello') will return the value None. When you assign that result to a reference like _, you're saving that None.
So, in the first snippet, you're yielding an object, then you save the "result" of that yield operation, which is None, and then you yield that None. In the second snippet, you yield an object, then you save the "result" of that yield operation, but you never yield that result, so None does not appear in the output.
Note that yield won't always return None - this is just what you sent to the generator with send(). Since that was nothing in this case, you get None. See this answer for more on send().
To expand on TigerhawkT3's answer, the reason that the yield operation is returning None in your code is because list(r1) isn't sending anything into the generator. Try this:
def test1(x):
for i in x:
_ = yield i
yield _
r1 = test1([1, 2, 3])
for x in r1:
print(' x', x)
print('send', r1.send('hello!'))
Output:
x 1
send hello!
x 2
send hello!
x 3
send hello!
Here's a somewhat manufactured example where sending values into a generator could be useful:
def changeable_count(start=0):
current = start
while True:
changed_current = yield current
if changed_current:
current = changed_current
else:
current += 1
counter = changeable_count(10)
for x in range(20):
print(next(counter), end=' ')
print()
print()
print('Sending 51, printing return value:', counter.send(51))
print()
for x in range(20):
print(next(counter), end=' ')
print()
print()
print('Sending 42, NOT printing return value')
print()
counter.send(42)
for x in range(20):
print(next(counter), end=' ')
print()
Output:
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
Sending 51, printing return value: 51
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
Sending 42, NOT printing return value
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62

2 different results when expecting the same..why?

When running the code, please see below, input is ('Zoe', 14), I get as result 8, running the 'Finding Buckets' code in the Online Python Tutor also with ('Zoe', 14), where "def hash_string" is included, the result is 2 out 2, when that code finished, why? Or, in other words, does the other 2 defs causing that result?
In the 'Finding Buckets' code are 3 def. I exchanged the order of those def - the results are the same-does the order really does not matter?
def hash_string(keyword,buckets):
out = 0
for s in keyword:
out = (out + ord(s)) % buckets
return out
Online Python Tutor "Finding Buckets":
1 def hashtable_get_bucket(table,keyword):
2 return table[hash_string(keyword,len(table))]
3
4 def hash_string(keyword,buckets):
5 out = 0
6 for s in keyword:
7 out = (out + ord(s)) % buckets
8 return out
9
10 def make_hashtable(nbuckets):
11 table = []
12 for unused in range(0,nbuckets):
13 table.append([])
14 return table
15 table = [[['Francis', 13], ['Ellis', 11]], [], [['Bill', 17],
16 ['Zoe', 14]], [['Coach', 4]], [['Louis', 29], ['Rochelle', 4], ['Nick', 2]]]
17 print hashtable_get_bucket(table, "Zoe")
def hashtable_get_bucket(table,keyword):
return table[hash_string(keyword,len(table))]
def hash_string(keyword,buckets):
out = 0
for s in keyword:
out = (out + ord(s)) % buckets
return out
def make_hashtable(nbuckets):
table = []
for unused in range(0,nbuckets):
table.append([])
return table
Here the comment from the notes:
Function hashtable_get_bucket returns the bucket containing the given keyword, from the hash
table, passed in as the first argument.
If you remember structure of a hash table, you will find out that it is composed of n buckets, one of
which needs to be returned by the hashtable_get_bucket function. Index of the bucket, which
would eventually contain the given keyword (in case the keyword will be present in the hash table),
is computed and returned by already defined function hash_string.
The function hash_string will in turn take the keyword and number of buckets as its
arguments. First argument (the keyword) is straightforward, since it was passed directly to
hashtable_get_bucket function by its caller. The second argument (number of buckets) can be
computed using len function on the hashmap (recall how hashmap is composed of n buckets).
Both Functions do exactly the same.
But in the online part hash_string('Zoe', 5) is called and not hash_string('Zoe', 14)
Where dose the 5 come from?
In line 2 there is:
hash_string(keyword, len(table))
with len(tabel) being 5.

How can I infinitely loop an iterator in Python, via a generator or other?

It's my understanding that using a Generator is the best way to achieve something like this, but I'm open to suggestions.
Specifically, one use case is this: I'd like to print some items alongside another list, of an arbitrary length, truncating the initial iterator as necessary.
Here is working python code that demonstrates the exact example behavior I desire:
def loop_list(iterable):
"""
Return a Generator that will infinitely repeat the given iterable.
>>> l = loop_list(['sam', 'max'])
>>> for i in range(1, 11):
... print i, l.next()
...
1 sam
2 max
3 sam
4 max
5 sam
6 max
7 sam
8 max
9 sam
10 max
>>> l = loop_list(['sam', 'max'])
>>> for i in range(1, 2):
... print i, l.next()
...
1 sam
"""
iterable = tuple(iterable)
l = len(iterable)
num = 0
while num < l:
yield iterable[num]
num += 1
if num >= l:
num = 0
The Problem / My Question
As you may have noticed, this only works on lists/tuples/iterables that implement __getitem__ (if I'm not mistaken). Ideally, I'd like to be able to pass any iterable, and receive a generator that can properly loop over it's content.
If there's a better way to do something like this without a generator, I'm fine with that as well.
You can use itertools.cycle (source included on linked page).
import itertools
a = [1, 2, 3]
for element in itertools.cycle(a):
print element
# -> 1 2 3 1 2 3 1 2 3 1 2 3 ...
Try this-
L = [10,20,30,40]
def gentr_fn(alist):
while 1:
for j in alist:
yield j
a = gentr_fn(L)
print a.next()
print a.next()
print a.next()
print a.next()
print a.next()
print a.next()
print a.next()
>>gentr_fn(x,y)
10 20 30 40 10 20 30 ...
You can use module to keep it simple.
Just make sure not to start iterating from 0.
my_list = ['sam', 'max']
for i in range(1, 100):
print(my_list[(i % len(my_list))-1])
You can iterate over a list, appending an item to it:
somelist = [item1, item2, item3]
for item in somelist:
somelist.append(item)
do_something()

Categories

Resources