List comprehensions are having some unexpected interactions with scoping. Is this the expected behaviour?
I've got a method:
def leave_room(self, uid):
u = self.user_by_id(uid)
r = self.rooms[u.rid]
other_uids = [ouid for ouid in r.users_by_id.keys() if ouid != u.uid]
other_us = [self.user_by_id(uid) for uid in other_uids]
r.remove_user(uid) # OOPS! uid has been re-bound by the list comprehension above
# Interestingly, it's rebound to the last uid in the list, so the error only shows
# up when len > 1
At the risk of whining, this is a brutal source of errors. As I write new code, I just occasionally find very weird errors due to rebinding -- even now that I know it's a problem. I need to make a rule like "always preface temp vars in list comprehensions with underscore", but even that's not fool-proof.
The fact that there's this random time-bomb waiting kind of negates all the nice "ease of use" of list comprehensions.
Yes, assignment occurs there, just like it would in a
for
loop. No new scope is being created.This is definitely the expected behavior: on each cycle, the value is bound to the name you specify. For instance,
Once that's recognized, it seems easy enough to avoid: don't use existing names for the variables within comprehensions.
some workaround, for python 2.6, when this behaviour is not desirable
Interestingly this doesn't affect dictionary or set comprehensions.
However it has been fixed in 3 as noted above.
Yes, list comprehensions "leak" their variable in Python 2.x, just like for loops.
In retrospect, this was recognized to be a mistake, and it was avoided with generator expressions. EDIT: As Matt B. notes it was also avoided when set and dictionary comprehension syntaxes were backported from Python 3.
List comprehensions' behavior had to be left as it is in Python 2, but it's fully fixed in Python 3.
This means that in all of:
the
x
is always local to the expression while these:in Python 2.x all leak the
x
variable to the surrounding scope.UPDATE for Python 3.8(?): PEP 572 will introduce
:=
assignment operator that deliberately leaks out of comprehensions and generator expressions! It's motivated by essentially 2 use cases: capturing a "witness" from early-terminating functions likeany()
andall()
:and updating mutable state:
See Appendix B for exact scoping. The variable is assigned in closest surrounding
def
orlambda
, unless that function declares itnonlocal
orglobal
.List comprehensions leak the loop control variable in Python 2 but not in Python 3. Here's Guido van Rossum (creator of Python) explaining the history behind this: