Search code examples
pythongenerator

Python yield expression misunderstanding


I was reading this article and the following program that I have found there is confusing me:

def is_palindrome(num):
    if num // 10 == 0:
        return False
    temp = num
    reversed_num = 0

    while temp != 0:
        reversed_num = (reversed_num * 10) + (temp % 10)
        temp = temp // 10

    if num == reversed_num:
        return True
    else:
        return False

def infinite_palindromes():
    num = 0
    while True:
        if is_palindrome(num):
            print("found a palindrome: "+str(num))
            j = (yield num)
            print("j = "+str(j))
            if j is not None:
                num = j
        print("num = "+str(num))
        num +=1

pal_gen = infinite_palindromes()
for i in pal_gen:
    print("i = "+str(i))
    digits = len(str(i))
    pal_gen.send(10 ** (digits))

The output starts with:

  • num = 0
  • num = 1
  • num = 2
  • num = 3
  • num = 4
  • num = 5
  • num = 6
  • num = 7
  • num = 8
  • num = 9
  • num = 10
  • found a palindrome: 11
  • i = 11
  • j = 100
  • num = 100
  • found a palindrome: 101
  • j = None
  • num = 101
  • num = 102
  • num = 103
  • num = 104
  • num = 105
  • num = 106

I do not understand why after found a palindrome: 101, there do not appear i = 101 and j = 1000?

My thoughts: The program starts. With for i in pal_gen:, the program starts to run the while loop in infinite_palindromes(). When then num=10 and num += 1, the if clause is_palindrome is fullfilled. Then, when the program reads "yield", it continuous with print("i = "+str(i)). Then, with pal_gen.send(10 ** (digits)) it goes back to the generator at the line j = (yield num) and assigns to j the value 100. Then it assigns to num also 100 and then increases num to 101. Because this is a palindrome, we are again in the if clause, so the program prints "found a palindrome: 101". The next line that the program reads and executes is now again j = (yield num). And I thought that it is the same situation as before, so I imagined that after reading "yield", the program continuous at print("i = "+str(i))...and so on.


Solution

  • The problem is that you are mixing regular iteration and send, this does not work: send does not just inject a value into the generator it also runs an entire iteration and returns the next value.

    So if you're using send, you need to perform the entire iteration by hand, using either only send (except for the first iteration which is necessarily a next) or next and send.

    Here because you're mixing send and regular iteration, you're skipping over every other iteration: an item gets extracted from the generator, then send is called, injects a signal, and the next value of the generator is skipped / dropped.

    This script shows the issue, the second snippet is what you are doing here:

    def iterable():
        v = 0
        while v < 42:
            n = yield v
            v = (v if n is None else n) + 1
            
    print("regular iteration")
    for i in iterable():
        print(i, end=" ")
    print()
    
    print("mix iteration and send")
    it = iterable()
    for i in it:
        print(i, end=" ")
        # you might thing this has no effect since it sends
        # in the same value it got out, but it skips every 
        # other value
        try:
            it.send(i)
        except StopIteration:
            break
    print()
    
    print("only send")
    it = iterable()
    i = next(it)
    while True:
        print(i, end=" ")
        # this is the correct way to handle it
        try:
            i = it.send(i)
        except StopIteration:
            break
    print()
    
    print("conditional send, mix with next")
    it = iterable()
    i = next(it)
    while True:
        print(i, end=" ")
        # if you mix next and send it's fine as long as
        # you're coherent and store every value
        try:
            if i % 2:
                i = next(it)
            else:
                i = it.send(i)
        except StopIteration:
            break
    print()
    

    Since you're send-ing on every iteration, snippet 3 ("only send") is what you need to do.