43

Can we implement yield or generator statement (with a loop) within a lambda?

My question is to clarify:

Whether the following simple loop function can be implemented with yield

def loopyield():
   for x in range(0,15):
      yield x
print(*loopyield())

Results in error:

lamyield=lambda x: yield x for x in range(0,15)
                       ^
SyntaxError: invalid syntax

Which looks like, it was expecting something as right operand for unwritten return statement but found the yield and getting confused.

Is there a proper legit way to achieve this in a loop?

Side note: yield can be statement/expression depending on who you ask: yield - statement or expression?

Final Answer : yield can be used with lambda but the limitation(single-line) makes it useless. for/while not possible in lambda because they are not expressions. -user2357112 implicit for loop is possible with list comprehension, and yield is valid within the list comprehension. -wim

Verdict- Explicit loops not possible because lambdas in python can only contain expressions, and to write an explicit loop you will need to use statements. -wim

theMobDog
  • 1,251
  • 3
  • 13
  • 26
  • 1
    "but you can also use a statement like `print()` as long as it is contained in a single line" - wrong! `print` is a function in Python 3, and `print` calls are ordinary expressions. You cannot use arbitrary single-line statements inside a `yield` call. – user2357112 Nov 14 '16 at 17:48
  • 7
    Trying to write this with a `lambda` is pointless. If you want to stuff it onto a single line, `(x for x in range(0, 15))` would be a direct genexp translation of your generator function. – user2357112 Nov 14 '16 at 17:51
  • I agree. I was trying to test a theory. On second thought, with the limitation of `lambda` having to fit into a single statement, I doubt I can achieve anything at all to have a loop within a `lambda`. But it will be good to finally clarify this because it has been bugging me. – theMobDog Nov 14 '16 at 17:53
  • So, no loop/or `yield` statement within a `lambda` ? Can we stay that for sure now? – theMobDog Nov 14 '16 at 17:57
  • Also, another thing that comes to mind, adding a yield statement to a fn makes it a generator obj. But looks like lambda creates/makes a regular function obj. (Please clarify if it can't be generator obj) – theMobDog Nov 14 '16 at 18:01
  • @MosesKoledoye: `yield` is an expression now.. – Martijn Pieters Nov 14 '16 at 18:04
  • 3
    While you can technically put a `yield` in a lambda function, the constraints of lambda functions make it essentially never a useful thing to do. – user2357112 Nov 14 '16 at 18:09
  • 3
    You'd have to parenthesise `(yield x)`. But the whole `for` loop syntax is not a valid expression anyway. – Martijn Pieters Nov 14 '16 at 18:27

4 Answers4

50

The one-liner you seem to be trying to create is actually technically possible with a lambda, you just need to help the parser a bit more:

>>> lamyield = lambda: [(yield x) for x in range(15)]
>>> print(*lamyield())
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

This uses a for loop implicitly in a list comprehension. It is not possible with an explicit while loop or for loop outside of a comprehension. That's because lambdas in Python can only contain expressions, and to write an explicit loop you will need to use statements.

Note: this syntax is deprecated in Python 3.7, and will raise SyntaxError in Python 3.8

bad_coder
  • 11,289
  • 20
  • 44
  • 72
wim
  • 338,267
  • 99
  • 616
  • 750
  • Yeah. Like someone pointed out, it is basically a generator obj. Do you know if it can be done with `for`/`while`. Also, you can't really do anything with lambda one-line. – theMobDog Nov 14 '16 at 18:21
  • 5
    Note that the results are completely different on Python 2. This code depends in a really subtle way on the fact that on Python 3, the list comprehension is implemented with *another* anonymous function inside the anonymous function created by `lambda`. – user2357112 Nov 14 '16 at 18:30
  • Wow, what is going on here? Could you explain what `[(yield x) for x in range(15)]` does? What does the expression return? – HelloGoodbye Sep 28 '18 at 12:56
  • It returns a generator object. When you iterate the generator you will get the values yielded during the iteration of the comprehension. When the iteration is complete, the `StopIteration` exception instance will have a `value` attribute. This will be the list comprehension result: it will be a list of length 15, and the elements of this list will be the items which were *sent* into the generator. – wim Sep 28 '18 at 14:27
  • 2
    you can also do something like this-- which lazily evaluates each function, and will NOT be deprecated in 3.8: `lamyield = lambda: ((yield expensive_func1()), (yield expensive_func2()), (yield expensive_func3()))` – Rick Mar 29 '19 at 14:01
  • An alternative would be `lambda: (x for x in range(15))` – levsa Jun 16 '21 at 08:39
16

Is it any necessity for using yield inside of lambda if you can rewrite it with generator such that?

In[1]: x = (i for i in range(15))
In[2]: x
Out[2]: <generator object <genexpr> at 0x7fbdc69c3f10>

In[3]: print(*x)
Out[3]: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

In[4]: x = (i for i in range(0, 15))
In[5]: x.__next__()
Out[5]: 0

In[6]: next(x)
Out[6]: 1
ncopiy
  • 1,515
  • 14
  • 30
6

You actually can loop through a lambda in useful ways, it's just that the example you provided isn't a great use case.

One instance where you might want to use yield inside a lambda might be to lazily execute expensive functions only when needed. Like so:

def expensive_check1():
    print("expensive_check1")
    return True


def expensive_check2():
    print("expensive_check2")
    return True


def expensive_check3():
    print("expensive_check3")
    return True


def do_the_thing(*args):
    print(args)


if __name__=="__main__":
    for check, args in (lambda: (
                                (yield (expensive_check1(), ["foo", "bar"])), 
                                (yield (expensive_check2(), ["baz"])),
                                (yield (expensive_check3(), [])),
                        ))():
        if check:
            do_the_thing(*args)
            continue
        raise Exception("oh noes!!!")
    else:
        print("all OK!")

Output:

expensive_check1
('foo', 'bar')
expensive_check2
('baz',)
expensive_check3
()
all OK!

Note that the expensive checks only happen at the start of each loop, rather than all at once. Also note that this syntax will still work in Python 3.8+, since it is not using the yield inside of a comprehension.

Rick
  • 43,029
  • 15
  • 76
  • 119
3

I have a simpler solution

lmbdgen = lambda: (x for x in range(15))
lmbdgen
Out[40]: <function __main__.<lambda>()>
lmbdgen()
Out[41]: <generator object <lambda>.<locals>.<genexpr> at 0x00000171473D8D60>
list(lmbdgen())
Out[42]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

This creates a function that returns a generator. This is a simpler way to access a generator multiple times.

For an alternative version

def defgen(): yield from (x for x in range(5))
def defgenyield(): return (x for x in range(5))

Here is the performance difference

def defgen_return(): return (x for x in range(10000))
def defgen_yield(): yield from (x for x in range(10000))
lmbdgen = lambda: (x for x in range(10000))

%timeit list(defgen_return())
384 µs ± 4.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit list(defgen_yield())
563 µs ± 9.43 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit list(lmbdgen())
387 µs ± 5.36 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)