即使在理解范围之后,列表理解也会重新绑定名称。是这样吗?

2024-05-21 08:07:54 发布

您现在位置:Python中文网/ 问答频道 /正文

理解与范围界定有着一些意想不到的互动。这是预期的行为吗?

我有个方法:

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

冒着抱怨的风险,这是错误的残酷根源。在我编写新代码时,我只是偶尔会发现由于重新绑定而导致的非常奇怪的错误——即使现在我知道这是个问题。我需要制定一个规则,比如“总是在列表理解中的临时变量前面加下划线”,但即使这样也不是傻瓜证明。

事实上,这种随机的定时炸弹等待否定了列表理解的所有“易用性”。


Tags: theinselfid列表foruidby
3条回答

列表理解泄漏了Python 2中的循环控制变量,但Python 3中没有。下面是Guido van Rossum(Python的创建者)explaining这背后的历史:

We also made another change in Python 3, to improve equivalence between list comprehensions and generator expressions. In Python 2, the list comprehension "leaks" the loop control variable into the surrounding scope:

x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'

This was an artifact of the original implementation of list comprehensions; it was one of Python's "dirty little secrets" for years. It started out as an intentional compromise to make list comprehensions blindingly fast, and while it was not a common pitfall for beginners, it definitely stung people occasionally. For generator expressions we could not do this. Generator expressions are implemented using generators, whose execution requires a separate execution frame. Thus, generator expressions (especially if they iterate over a short sequence) were less efficient than list comprehensions.

However, in Python 3, we decided to fix the "dirty little secret" of list comprehensions by using the same implementation strategy as for generator expressions. Thus, in Python 3, the above example (after modification to use print(x) :-) will print 'before', proving that the 'x' in the list comprehension temporarily shadows but does not override the 'x' in the surrounding scope.

是的,赋值发生在那里,就像在for循环中一样。没有创建新作用域。

这绝对是预期的行为:在每个周期中,值都绑定到指定的名称。例如

>>> x=0
>>> a=[1,54,4,2,32,234,5234,]
>>> [x for x in a if x>32]
[54, 234, 5234]
>>> x
5234

一旦识别出这一点,似乎就很容易避免:不要对理解中的变量使用现有的名称。

是的,在Python2.x中列出理解“泄漏”它们的变量,就像for循环一样。

回想起来,这被认为是一个错误,并且通过生成器表达式避免了这个错误。EDIT:AsMatt B. notes当set和dictionary comprehension语法从Python 3后移植时,也避免了这种情况。

列表理解的行为必须保持在Python 2中的状态,但在Python 3中它是完全固定的。

这意味着:

list(x for x in a if x>32)
set(x//4 for x in a if x>32)         # just another generator exp.
dict((x, x//16) for x in a if x>32)  # yet another generator exp.
{x//4 for x in a if x>32}            # 2.7+ syntax
{x: x//16 for x in a if x>32}        # 2.7+ syntax

在以下情况下,x始终是表达式的本地:

[x for x in a if x>32]
set([x//4 for x in a if x>32])         # just another list comp.
dict([(x, x//16) for x in a if x>32])  # yet another list comp.

在Python 2.x中,all将x变量泄漏到周围的作用域。


Python 3.8的更新(?)PEP 572将引入:=赋值运算符,该运算符故意泄漏理解和生成器表达式之外的信息!它的动机基本上是2个用例:从早期终止的功能(如any()all())中捕获“见证”:

if any((comment := line).startswith('#') for line in lines):
    print("First comment:", comment)
else:
    print("There are no comments")

以及更新可变状态:

total = 0
partial_sums = [total := total + v for v in values]

请参见Appendix B了解确切范围。变量在最接近的deflambda中赋值,除非该函数声明它nonlocalglobal

相关问题 更多 >