I frequently take a look into the Python discussion forum, mainly to the "ideas" section, where people propose things they would like to see in the language or in libraries. Though sadly most of the time most requests for additions to the language are promptly rejected (sometimes with absurd reaons, as if given by people that do not use other modern languages...) the proposed alternatives and the ideas themselves can be pretty interesting. Sometime ago I came across an idea that I was not immediatelly grasping, Literal syntax for generatos.
We know generators generate values lazily, as requested. But if we had a literal syntax we would be already providing all values, so why not just use a list? Well, because the idea would be supporting that some values are generated by functions, that get invoked when the iteration requests it. Well, the guy showed the workaround that he's using, and it's pretty, pretty, interesting:
queries: Iterator[str] = (lambda: (
(yield "abc"),
(yield t if (t := expensive_func()) else None),
(yield "something else"),
))()
Honestly it took me quite a while to understand what's going on there. So he's using an immediatelly invokable lambda with an expression containing multiple expressions (this is the trick that I explained some time ago), where each subexpression is yielding a value. In Python yield can be used both as a statement and as an expression. The clear case for the latter is when we write a = yield "whatever", for using with the send(value) method). In this example, for the yield to be considered as an expression it has to be wrapped with parentheses.
It looks pretty nice, and it would have never occurred to me. The alternative that I could have envisioned would be a generator factory like this:
def lazy_gen(values: Iterable) -> Generator:
"""
returns a Generator object
"""
def gen_fn_wrapper():
for it in values:
yield it() if callable(it) else it
return gen_fn_wrapper()
Indeed, maybe this approach is easier to understand. Let's see an example:
def expensive_format(msg: str):
sleep(2)
return msg.upper()
print("started")
messages: Generator[str, None, None] = (lambda: (
(yield "abc"),
(yield expensive_format("my message")),
(yield expensive_format("my message 2")),
(yield "something else"),
))()
for message in messages:
print(message)
print("- second approach")
messages = lazy_gen([
"abc",
lambda: expensive_format("my message"),
partial(expensive_format, "my message 2"),
"something else",
])
for message in messages:
print(message)
There's another approach that they mention in the discussion thread and that uses a trick that I find rather confusing. We can use a decorator to immediatelly invoke a function that we've just defined. This means that we can define a normal generator function that contains statements and invoke it to obtain a generator object that will get assigned to a variable with the name of the function that we've just decorated. As I've said, it feels terribly confusing, but I'll include it here for the sake of completion. I have to say that defining the decorator inline with a lambda feels pretty cool :-) Let's see:
@lambda x: x() # Or @operator.call
def messages():
yield "abc"
yield expensive_format("my message")
yield expensive_format("my message 2")
yield "something else"
for message in messages:
print(message)
yield expressions are, well, expressions, which means that in principle we can use them anywhere an expression can be used, for example as argument to a function. I mean
def f1():
yield "a"
print(str((yield "b")))
yield "d"
gen = f1()
print(next(gen))
#'a'
print(next(gen))
#'b'
print(gen.send(11))
#11
#'d'
No comments:
Post a Comment