Is there a way to access yields inside the same function in Python?

Question:

I am currently trying to replace the manual counting of yields by a built-in attribute:

def random_select(iterations):
    count = 0
    for index in range(iterations):
        if random.randint(0, 9) > 4:
            yield index
            count += 1
    assert count > 0

Something I am looking for:

def random_select(iterations):
    for index in range(iterations):
        if random.randint(0, 9) > 4:
            yield index
    assert len(self.__yield__.__results__) > 0

Is there something alike in Python?

Asked By: TheRealVira

||

Answers:

In Python, "yield from" makes it possible to string generators together. It behaves the same way as "yield", except that it delegates the generator machinery to a sub-generator. For a better understanding, look at the code below. It allows the yield expression to be moved out of the main generator, making refactoring easier.

def subgen(x):
    for i in range(x):
        yield i

def gen(y):
    yield from subgen(y)

for q in gen(6):
    print(q)

The output would be 0 to 5.

Answered By: Dorrin Samadian

Python has no such feature.

However, if you invert the structure of your code a bit, iterating over successfully generated in-range values instead of range(iterations), you can simply use enumerate.

def random_select(iterations):
    values = (x 
              for x in (random.randint(0, 9) for _ in range(iterations))
              if x > 4)

    count = 0
    for count, v in enumerate(values, start=1):
        yield v
    assert count

(This is only the slightest improvement over what you already have, though, in that you don’t have to manually increment count.)

Answered By: chepner

You could use generator.send() and chain a few generators together.

import random

def inner(iterations, iterated=False):
    for index in range(iterations):
        if random.randint(0, 9) > 4:
            iterated = yield
            yield index
    assert iterated

def random_select(iterations):
    gen = inner(iterations)
    yield from (gen.send(True) for _ in gen)

for i in random_select(2):
    print(i)
Answered By: Axe319

If you do this more often and the objective is less code and you don’t mind it getting slower, you could use a decorator that wraps your generator in another that asserts the given check afterwards:

@assert_afterwards(list_check=lambda yielded: len(yielded) > 0)
def random_select(iterations):
    for index in range(iterations):
        if random.randint(0, 9) > 4:
            yield index

Though for checking len > 0, you wouldn’t need to keep track of all yielded values but just of their count or even just of whether there were any yields:

@assert_afterwards(count_check=lambda yielded: yielded > 0)
def random_select(iterations):
    for index in range(iterations):
        if random.randint(0, 9) > 4:
            yield index
@assert_afterwards(any_check=True)
def random_select(iterations):
    for index in range(iterations):
        if random.randint(0, 9) > 4:
            yield index

Possible implementation of that decorator (could be improved with better messages for failed assertions and at least the "any" version could be made much faster), including demo:

import random
random.seed(0)

def assert_afterwards(any_check=None, count_check=None, list_check=None):
    def deco(gen):
        if any_check:
            def gen_any(*args):
                yielded = False
                for value in gen(*args):
                    yield value
                    yielded = True
                assert yielded
            return gen_any
        if count_check:
            def gen_count(*args):
                yielded = 0
                for value in gen(*args):
                    yield value
                    yielded += 1
                assert count_check(yielded)
            return gen_count
        def gen_list(*args):
            yielded = []
            for value in gen(*args):
                yield value
                yielded.append(value)
            assert list_check(yielded)
        return gen_list
    return deco

#@assert_afterwards(list_check=lambda yielded: len(yielded) > 0)
#@assert_afterwards(count_check=lambda yielded: yielded > 0)
@assert_afterwards(any_check=True)
def random_select(iterations):
    for index in range(iterations):
        if random.randint(0, 9) > 4:
            yield index

for _ in range(100):
    for x in random_select(5):
        print(x, flush=True, end=' ')
    print()

Try it online!

Answered By: Kelly Bundy
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.