即使在理解范围之后,列表理解也会重新命名.这是正确的吗?

Jab*_*ams 116 python binding list-comprehension

理解与范围界定有一些意想不到的相互作用.这是预期的行为吗?

我有一个方法:

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
Run Code Online (Sandbox Code Playgroud)

冒着抱怨的风险,这是一个残酷的错误来源.当我编写新代码时,我偶尔会发现由于重新绑定而导致非常奇怪的错误 - 即使现在我知道这是一个问题.我需要制定一个规则,比如"总是用下划线列出列表推导中的临时变量",但即使这样也不是万无一失的.

这种随机定时炸弹等待的事实否定了列表理解的所有"易用性".

Ste*_*ski 167

List comprehensions在Python 2中泄漏了循环控制变量,但在Python 3中没有泄漏.这里是Guido van Rossum(Python的创建者)解释了这背后的历史:

我们还在Python 3中进行了另一项更改,以改进列表推导和生成器表达式之间的等效性.在Python 2中,列表推导将循环控制变量"泄漏"到周围的范围中:

x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'
Run Code Online (Sandbox Code Playgroud)

这是列表推导的原始实现的工件; 多年来它一直是Python"肮脏的小秘密"之一.它起初是一种故意的妥协,使列表理解能够快速地进行,虽然它对于初学者来说不是常见的陷阱,但它偶尔会刺激人们.对于生成器表达式,我们不能这样做.生成器表达式使用生成器实现,生成器的执行需要单独的执行帧.因此,生成器表达式(特别是如果它们在短序列上迭代)的效率低于列表推导.

但是,在Python 3中,我们决定使用与生成器表达式相同的实现策略来修复列表推导的"脏小秘密".因此,在Python 3中,上面的例子(修改后使用print(x):-)将打印'before',证明列表理解中的'x'暂时阴影但不覆盖周围的'x'范围.

  • 另请注意,现在在2.7中,set和dictionary comprehensions(和生成器)具有私有范围,但列表推导仍然没有.虽然这有点意义,因为前者都是从Python 3反向移植的,但实际上它与列表推导的对比有些不和. (36认同)
  • 我要补充一点,虽然Guido称之为"肮脏的小秘密",但很多人认为它是一个功能,而不是一个bug. (14认同)
  • 我知道这是一个非常古老的问题,但_why_有人认为它是该语言的一个特征吗?有什么东西支持这种变量泄漏吗? (7认同)
  • **for:loops**泄漏有充分的理由,尤其是 在早期"休息"之后访问最后一个值 - 但与复合无关.我记得一些comp.lang.python讨论,人们想在表达式中间分配变量.发现的*少疯狂*方式是条款的单一值,例如.对于[s + i]] [ - 1]中的s,`sum100 = [s代表[0]中的s代表i的范围内(1,101),但只需要一个理解局部变量,并且在Python中也能正常工作3.我认为"泄漏"是在表达式外设置变量的唯一方法.大家都认为这些技术太可怕了:-) (2认同)
  • 这里的问题是无法访问列表推导式的周围范围,而是在列表推导式范围内绑定会影响周围的范围。 (2认同)

Ben*_*kin 47

是的,列表推导在Python 2.x中"泄漏"它们的变量,就像for循环一样.

回想起来,这被认为是一个错误,并且通过生成器表达式避免了这一点.编辑:正如 马特B.注意到,当设置和字典理解语法从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
Run Code Online (Sandbox Code Playgroud)

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.
Run Code Online (Sandbox Code Playgroud)

在Python 2.x中,所有内容都将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")
Run Code Online (Sandbox Code Playgroud)

并更新可变状态:

total = 0
partial_sums = [total := total + v for v in values]
Run Code Online (Sandbox Code Playgroud)

有关确切范围,请参阅附录B. 变量在最近的周围分配,def或者lambda,除非该函数声明它nonlocalglobal.


JAL*_*JAL 7

是的,在那里进行分配,就像它在for循环中一样.没有创建新的范围.

这绝对是预期的行为:在每个循环中,值绑定到您指定的名称.例如,

>>> x=0
>>> a=[1,54,4,2,32,234,5234,]
>>> [x for x in a if x>32]
[54, 234, 5234]
>>> x
5234
Run Code Online (Sandbox Code Playgroud)

一旦认识到这一点,似乎很容易避免:不要在理解中使用现有的变量名称.