0

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?

TheRealVira
  • 1,444
  • 4
  • 16
  • 28
  • No built-in attribute like that exists. No built-in attribute like that *can* exist, because it would defeat the unbounded nature of generator functions. – jasonharper Jul 15 '22 at 15:34
  • It *could* exist, it just doesn't. (It's not trying to predict how many times the generator *will* yield, only report how many times it *has* yielded so far.) – chepner Jul 15 '22 at 15:37
  • 3
    Your code doesn't make sense: You throw a die `iterations` times and then `assert` that at least one toss was greater than a certain value. Ignoring the corner case `iterations = 0`, there still is no guarantee that you ever get a single value greater than some limit. Can you clarify what you want there? – Ulrich Eckhardt Jul 15 '22 at 15:49
  • Are you asking exactly about counting? Or about accessing *all* yields? Or just about *whether* there was *any* yield? And what's the objective, less code or speed? – Kelly Bundy Jul 15 '22 at 16:48

4 Answers4

1

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.

  • Hey welcome to StackOverflow! Nice to have you with us and thank you for submitting your solution. I didn't know of the "from" keyword yet, so that's definitely a plus. – TheRealVira Jul 15 '22 at 15:45
  • 1
    There's no indication following the `yield from ...` expression whether or not anything was, in fact, yielded. – chepner Jul 15 '22 at 15:52
  • Yeah, although it isn't used a lot these days, it started from Python 3.3 This link might help you dude :) https://stackoverflow.com/questions/9708902/in-practice-what-are-the-main-uses-for-the-yield-from-syntax-in-python-3-3 – Dorrin Samadian Jul 15 '22 at 16:02
1

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.)

chepner
  • 497,756
  • 71
  • 530
  • 681
  • 2
    Whoever upvoted, upvoted prematurely :) The initial answer was a mess. – chepner Jul 15 '22 at 15:45
  • I did, because I like what your thought process is like. I might do this, but I am not too keen on doing two iterations. Maybe I'll figure something out. – TheRealVira Jul 15 '22 at 15:48
  • It's still wrong, too. You would at least have to initialize `count` before the loop to guarantee it is defined. There aren't really two iterations, though: no iteration occurs when defining `values`, only once you start iterating over it. The first time you call `next(values)`, for example, is the first time `filter` tries to call `next` on the generate expression that iterates over `range(iterations)`. One iteration, but nested levels of `next` calling `next`. – chepner Jul 15 '22 at 15:51
  • 2
    I'm not sure there's anything particularly more elegant than just maintaining `count` as you already are. – chepner Jul 15 '22 at 15:51
  • You could use zero-based enumeration and skip `count = 0`, but then the simpler `assert count` turns into `try: count except NameError: assert False`. IMO treating `NameError`s as flow control is decidedly worse than the previously longer code. – chepner Jul 15 '22 at 16:14
  • Hmm, you have the same `count = 0` initialization, the same final usage (if they also remove the `> 0`), so you just replaced their simple `count += 1` with more complicated and costly code. I don't think this is even the slightest improvement. I agree with your comment, I think *for counting* there's nothing better than what they have (unless they do that multiple times, in which case a decorator could at least save code). – Kelly Bundy Jul 15 '22 at 16:28
1

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)
Axe319
  • 4,255
  • 3
  • 15
  • 31
1

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!

Kelly Bundy
  • 23,480
  • 7
  • 29
  • 65