Since comprehension is now inline-ed, shall python allow "yield inside comprehension" again?

Expected behavior:

>>> def pack_b():
        while True:
            L = [(yield) for i in range(2)]
            print(L)

>>> pb = pack_b()
>>> next(pb)
>>> pb.send(1)
>>> pb.send(2)
[1, 2]
>>>
2 Likes

I’ve made a demo: GitHub - yunline/cpython at comp-yield
If this idea is good, I’m glad to open a pull request.

It was possible to properly support yield inside comprehension when it was not inlined. It is easy, the compiler only needs to add yield from when call the implicit internal function:

def pack_b():
        while True:
            def _compr(_arg):
                _result = []
                for i in _arg:
                    _result.append(yield)
                return _result
            L = yield from _compr(iter(range(2)))
            print(L)

But it was said not to do this.

It’s not that it was impossible or difficult to implement. It was decided not to implement it for other reasons. You must show that these reasons are no longer valid or that the other reasons are more important.

Thanks for your reply.

I’ve read the issue, but I still don’t know why “raising a SyntaxError” is a better solution than some fix like “add an implicit yield from”.

I’m sorry, could you please point the reason out?

The reason I would give is that there is an expectation that comprehensions are pure and work in “one go”. I feel like adding a yield breaks both expectations, but I don’t feel particularly strongly about it.

I am rather sympathetic to this idea (I implemented similar idea for await and async for in nested comprehensions). But, it seems, there were no good reasons for its implementation. And it is difficult to do this in generator expressions. What does ((yield) for i in range(2)) even mean?

I think it make sence to forbid yield inside a genexpr.
It’s mentioned in docs:

To avoid interfering with the expected operation of the generator expression itself, yield and yield from expressions are prohibited in the implicitly defined generator.

In my demo branch, “yield in generator expressions” still raises a SyntaxError.


About reasons:

Expressions like [(yield) for i in range(2)] are not ambiguous. They are straightforward, intuitive and make the comprehension expressions more flexible. Why not add them back? Especially when there is few difficulty on implementing this.

(Btw, I’ve been writing a tool to convert python scripts into oneliner expressions. The implementation of “oneliner yield” is largely determined by whether python allows “yield inside comprehension”. With “yield in comp”, the output code can be largely simplified. This is the direct reason why I want “yield in comp” XD.)

But my intuition is that [(yield) for i in range(2)] should mean exactly the same as list((yield) for i in range(2)). So if yield in a genexpr is forbidden, I’d expect yield in a listexpr to be as well.

To give a concrete example, if I have code that does something like [x*2 for x in range(10)] and I realise I need an immutable value, I’d simply turn that into tuple(x*2 for x in range(10)). If that transformation didn’t always work, I’d find it annoying and confusing. Conceded, I can’t imagine using yield in that situation[1] but consistency and reliable invariants are important.


  1. TBH, I don’t think I’ve ever actually used a yield expression ↩︎

3 Likes

This intuition does not work with asynchronous comprehensions and generators.
[await expr for x in iterable]
and
[expr async for x in iterable]
are working and pretty common code. But
list(await expr for x in iterable)
or
list(expr async for x in iterable)
unfortunately do not work and cannot work in the current Python execution model.

The difference is that it was not yet shown that there is a significant demand of yield in comprehensions. It it was so, there should be many examples of generator functions that create, populate in a loop and return lists, sets or dicts, and which could be easily transformed to comprehensions with yield. But I do not know any of such examples.

And there would need to be some reason that yield from was insufficient for these examples.

I can’t really think of why one would want to create a list comprehension of yields when yield from (genexpr) is possible.

If I’m not wrong, it’s not possible to get the send() value from a genexpr.
So, for most of cases that “yield in comp” used, they couldn’t be rewritten in yield from (genexpr) version because they used the value returned from yield

Here is one example:
def gen0():
    l = []
    for i in range(10):
        l.append((yield i))
    print(f"gen0: {l}")


def gen1():
    l = [(yield i) for i in range(10)]
    print(f"gen1: {l}")


def run(gen):
    it = iter(gen)
    last_value = next(it)
    while 1:
        try:
            last_value = it.send(str(last_value))
        except StopIteration:
            break

>>> run(gen0())
gen0: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
>>> run(gen1())
gen1: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
>>>

Okay, but why would you want to write such code? gen0 seems much more reasonable if you actually want do something with the sent values rather than just collect them–and if you just want to collect them, why send them?

Maybe there’s a realistic example where this would be useful?

One specific example:

def non_recursion(gen):
    def _non_recursion(*args):
        stack = [gen(*args)]
        converted = None
        while stack:
            try:
                sub_args = stack[-1].send(converted)
                stack.append(gen(*sub_args))
                converted = None
            except StopIteration as err:
                converted = err.value
                stack.pop()
        return converted

    return _non_recursion

I wrote a non_recursion decorator. Add the decorator to the recursive function, repace the self-calls with yield expressions and then we got a non-recursion function with the same functionality as before.

def reverse_list_1(_list):
    return [reverse_list_1(sub) if isinstance(sub, list) else sub for sub in reversed(_list)]

@non_recursion
def reverse_list_2(_list):
    return [(yield [sub]) if isinstance(sub, list) else sub for sub in reversed(_list)]


test_list = [1, 2, [6, 5, 4, [9, 8, [10, 11, 12, 13], 6]], 4]
print(reverse_list_1(test_list))
print(reverse_list_2(test_list))

# both print [4, [[6, [13, 12, 11, 10], 8, 9], 4, 5, 6], 2, 1]

In this specific case (reverse_list), without “yield in comp”, it’s not possible to convert the function in such “low cost” way. We have to rewrite the comprehension as a for loop since “yield in comp” is forbidden.

Don’t show us the code which you could write if “yield” be supported in comprehensions. Show an existing code in the form:

def gen(...):
    result = []
    for ...:
        result.append(... yield ...)
    return result

If there is enough number of such examples in the real world code, it can be an argument for implementing this feature.

Okay, I’m trying to search on github and get a list of links of code.
Btw, I think pattern like for ...: result.append(... yield from ...) should be included too.

Cases of append(...(yield)...) (59 in total)

https://github.com/qwj/python-proxy/blob/7c967309d9224aced72202e141fba7ed479d6c40/pproxy/cipherpy.py#L183

https://github.com/saghul/asyncio-redis/blob/fd0ac6f267c1884dd8d43517d0aa2bc85dcb11f6/asyncio_redis/replies.py#L245

https://github.com/saghul/asyncio-redis/blob/fd0ac6f267c1884dd8d43517d0aa2bc85dcb11f6/tests.py#L832

https://github.com/seppeljordan/parsemon2/blob/c9534e1b836be9bbffc371ee48437180f3f4e682/src/parsemon/parser.py#L243

https://github.com/sfstpala/v6wos/blob/acb4483ab89174529172c3705a4ecbc463c938f5/v6wos/model/hosts.py#L19

https://github.com/todd-placher/bwscanner/blob/6191453e04d59053c6f49d08a74393463e9983d0/bwscanner/measurement.py#L137

https://github.com/tpwrules/tasha_and_friends/blob/9994f6e147febf671503882e545baae9b0bca80b/chrono_figure/eventuator/sim/test.py#L48

https://github.com/uf-mil-archive/SubjuGator/blob/c0f4d5296a33f939ede2784cd9297daea929513e/sub_launch/src/sub_launch/missions/recovery.py#L61

https://github.com/williamhogman/pasteshare/blob/ee641fdc0d2411537f65be1f6190964a65aef82c/pasteshare/model.py#L137

Cases of append(...(yield from)...) (19 in total)

https://github.com/CenterForOpenScience/waterbutler/blob/b9e11c8667546e7763752402a44ec8e15cfdbc52/waterbutler/providers/googledrive/provider.py#L435

https://github.com/DriverX/aioredis-cluster/blob/ab3baa687bffa1e72fce3987f03931f934057a23/src/aioredis_cluster/_aioredis/parser.py#L143

https://github.com/adamcharnock/lightbus/blob/cf892779a9a9a8f69c789ffa83c24acfb7f9a336/lightbus_vendored/aioredis/parser.py#L139

https://github.com/bslatkin/pycon2014/blob/bf70f1c00a6e38a1463b80dfc3f9064c7a5cc3a8/e08asyncparallel.py#L28

https://github.com/chrisseto/Still/blob/3e4df26b824227472e5f487905779deafc76b4dd/wdim/server/api/v1/document.py#L52

https://github.com/dragonteros/unsuspected-hangeul/blob/c73b6186eddecb568834a53ba084c262b53dfca5/pbhhg_py/utils.py#L62

https://github.com/evandroforks/JSCustom/blob/9b76fdbfd8b50d7edbf0115a36b577825e7ea930/src/syntax/macros.py#L14

https://github.com/lihuanshuai/thriftpy2/blob/2a5e1915a6ac2e294d769cb21cd613fa388b14bd/thriftpy2/contrib/aio/protocol/compact.py#L220

https://github.com/mars-project/mars/blob/217567800c0a69f11c4c75bb418972bd04a9f809/mars/tensor/einsum/core.py#L100

https://github.com/martexcoin/colorcore/blob/ab5e9eceb3a66eb59d20e1137488a46a5820bfcb/colorcore/operations.py#L332

https://github.com/methane/gunicorn/blob/ce1b29f66b87a6e432f25973aff2bac58784e8c0/gunicorn/workers/gaiohttp.py#L77

https://github.com/munhyunsu/Hobby/blob/5fada2449eb8d0390f85377246a1cf80fd11aed7/Coroutines/ex04.py#L11

https://github.com/rshorey/moxie/blob/0fac956c04d7054af4a435a0d383a99e158be8ec/moxie/app.py#L54

https://github.com/seandstewart/redis-py-sansio/blob/8d1a1734aa5744beadca82a65cb5a928a60f4061/sansredis/sansio/_parser.py#L204

https://github.com/sixstars/xcat/blob/1e568d36bb9540069601459abaaa95a6cdcfce08/xcat/lib/executors/xpath1.py#L84

https://github.com/sixstars/xcat/blob/1e568d36bb9540069601459abaaa95a6cdcfce08/xcat/xcat.py#L384

https://github.com/thomasballinger/dast/blob/056f23f38c791cf505c13210e4e92ebd27466233/gen_iter.py#L119

https://github.com/wabu/zeroflo/blob/a10a914e0b74b487f948ba88bd068b8454b7e700/zeroflo/flows/read/ressource.py#L103

https://github.com/wijnen/supernovel/blob/4a38ec9cc6d67397322a19f8471936e09927df04/supernovel#L189

I got these data by manually searching on github. Forks and duplicates are not counted. Because of the limitation of manual search, the real numbe of cases should be way larger than the number I got.

yield_cases.txt

↑ Here I’ve got another 504 cases.

Regular expression pattern:

r"for .* in .*:(([\n].*){0,10}).*\.(add|append)\(.*yield (from )*.*\)"