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