Creating lambdas inside generator expression

Peter Otten __peter__ at web.de
Thu Jun 30 06:04:13 EDT 2022


On 29/06/2022 23:17, Chris Angelico wrote:
> On Thu, 30 Jun 2022 at 02:49, Johannes Bauer <dfnsonfsduifb at gmx.de> wrote:
>> But now consider what happens when we create the lambdas inside a list
>> comprehension (in my original I used a generator expresison, but the
>> result is the same). Can you guess what happens when we create conds
>> like this?
>>
>> conds = [ lambda msg: msg.hascode(z) for z in ("foo", "bar") ]
>>
>> I certainly could not. Here's what it outputs:
>>
>> Check for bar
>> False
>> Check for bar
>> False
>>
>> I.e., the iteration variable "z" somehow gets bound inside the lambda
>> not by its value, but by its reference. All checks therefore refence
>> only the last variable.
>>
>
> Yep, that is the nature of closures. (Side point: This isn't actually
> a generator expression, it's a list comprehension; current versions of
> Python treat them broadly the same way, but there was previously a
> difference in the way scoping worked.) What you're seeing is a
> consequence of the way that closures work, and it is a very good thing
> most of the time :)
>
> The usual way to "snapshot" a variable is what you showed in your
> followup: a default argument value.
>
> def f(..., z=z):
>      ... z has been snapshot
>
> (As others have pointed out, this isn't unique to lambdas; any
> function will behave that way.)
>
> Antoon offered another variant, but written as a pair of lambda
> functions, it's a little hard to see what's going on. Here's the same
> technique written as a factory function:
>
> def does_it_have(z):
>      return lambda msg: msg.hascode(z)
>
> conds = [does_it_have(z) for z in ("foo", "bar")]
>
> Written like this, it's clear that the variable z in the comprehension
> is completely different from the one inside does_it_have(), and they
> could have different names if you wanted to. This is a fairly clean
> way to snapshot too, and has the advantage that it doesn't pretend
> that the function takes an extra parameter.

While I'd go with Chris' suggestion there are two other options:
functools.partial() and operator.methodcaller().

Example:

 >>> class Msg:
	def __init__(self, msg):
		self.msg = msg
	def hascode(self, code):
		return code in self.msg


 >>> conds = [partial(lambda z, msg: msg.hascode(z), z) for z in ("foo",
"bar")]
 >>> [cond(Msg("barbaz")) for cond in conds]
[False, True]
 >>> conds = [methodcaller("hascode", z) for z in ("foo", "bar")]
 >>> [cond(Msg("barbaz")) for cond in conds]
[False, True]




More information about the Python-list mailing list