4

In the below simplified code, I would like to reuse a loop to do a preparation first and yield the result.

However, the preparation (bar()) function is never executed.

Is yield statement changing the flow of the function?

def bar(*args,**kwargs):
    print("ENTER bar")
    pass

def foo(prepare=False):
    print("ENTER foo")
    for x in range(1,10):
        if prepare:
            bar(x)
        else:
            yield x


foo(prepare=True)

r = foo(prepare=False)
for x in r:
    pass
Chris_Rands
  • 38,994
  • 14
  • 83
  • 119
Boying
  • 1,404
  • 13
  • 20
  • 2
    Simply containing a `yield` does indeed change the nature of the function. It won't run like a normal function, even if you don't hit a `yield` command. – khelwood Feb 08 '17 at 09:31
  • @khelwood interesting, can you elaborate? – Chris_Rands Feb 08 '17 at 09:38
  • 1
    @Chris_Rands I think the code in the question covers it pretty well. Because the `foo` definition contains a `yield`, it won't run like a normal function even if you call it like one `foo(prepare=True)`. I think this is part of the reason the [new coroutine syntax](https://docs.python.org/3/library/asyncio-task.html) puts a keyword at the start of the definition, so that the change in nature isn't hidden inside the body of the function. – khelwood Feb 08 '17 at 09:41
  • @khelwood Thanks, I see – Chris_Rands Feb 08 '17 at 09:45

3 Answers3

3

Because the foo definition contains a yield, it won't run like a normal function even if you call it like one (e.g. foo(prepare=True) ).

Running foo() with whatever arguments will return a generator object, suitable to be iterated through. The body of the definition won't be run until you try and iterate that generator object.

The new coroutine syntax puts a keyword at the start of the definition, so that the change in nature isn't hidden inside the body of the function.

khelwood
  • 55,782
  • 14
  • 81
  • 108
  • Nice, +1, so `bar(x)` is actually stored in a generator too, even though it's not explicitly `yield`ed? – Chris_Rands Feb 08 '17 at 09:47
  • @Chris_Rands: Generators are initially paused; you start them with `.send(None)` and then they stop at each `yield`. – Ry- Feb 08 '17 at 09:52
0

The problem is that having a yield statement changes the function to returning a generator and alters the behavior of the function.

Basically this means that on the call of the .next function of the generator the function executes to the yield or termination of the function (in which case it raises StopIteration exception).

Consequently what you should have done is to ensure that you iterate over it even if the yield statement won't be reached. Like:

r = foo(prepare=True)
for x in r:
    pass 

In this case the loop will terminate immediately as no yield statement is being reached.

skyking
  • 13,817
  • 1
  • 35
  • 57
-2

In my opinion, the actual explanation here is that:

Python evaluates if condition lazily!

And I'll explain:

When you call to

foo(prepare=True)

just like that, nothing happens, although you might expected that bar(x) will be executed 10 times. But what really happen is that 'no-one' demanding the return value of foo(prepare=True) call, so the if is not evaluated, but it might if you use the return value from foo.

In the second call to foo, iterating the return value r, python has to evaluate the return value,and it does, and I'll show that:

Case 1

r = foo(prepare=True)
for x in r:
    pass

The output here is 'ENTER bar' 9 times. This means that bar is executed 9 times.

Case 2

r = foo(prepare=False)
for x in r:
    pass

In this case no 'ENTER bar' is printed, as expected.


To sum everything up, I'll say that:

for example:

# builds a big list and immediately discards it
sum([x*x for x in xrange(2000000)])

vs.

# only keeps one value at a time in memory
sum(x*x for x in xrange(2000000))

About lazy and eager evaluation in python, continue read here.

Community
  • 1
  • 1
Gal Dreiman
  • 3,969
  • 2
  • 21
  • 40