【问题标题】:Why does Python require for clauses in list comprehensions to be the wrong way around?为什么 Python 要求列表推导中的 for 子句是错误的?
【发布时间】:2013-04-02 14:29:18
【问题描述】:

看看两个列表推导,每个都包含两个 for 子句。我们看到,如果 for 子句的顺序正确,Python 就会感到困惑。但如果它们是错误的方法,Python 可以处理它。为什么?

Python 3.2.3 (default, Apr 11 2012, 07:15:24) [MSC v.1500 32 bit (Intel)] on win
32
Type "help", "copyright", "credits" or "license" for more information.
>>> [x + y for x in range(y) for y in range(4)]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'y' is not defined
>>> [x + y for y in range(4) for x in range(y)]
[1, 2, 3, 3, 4, 5]
>>>

【问题讨论】:

  • 在理解中写fors,就像你写普通循环一样:外部优先,内部接下来。
  • 我可以看到这就是 Python 所需要的。我要问的是为什么会这样。
  • 您将其视为“第一个循环,然后是第二个循环”。把它想象成“外循环,然后是内循环”
  • 正是出于这个原因,我避免使用嵌套列表组合...
  • @Hammerite 我并不是说没有理由按照您喜欢的顺序进行操作。但也有理由反过来做(一个已经被多次给出:它与等效的 for: 循环的顺序匹配)。这是我有问题的“错误”和“正确”。

标签: python list list-comprehension


【解决方案1】:

[x + y for x in range(y) for y in range(4)]

range(y) 中的 y 在那个地方是未知的。相当于:

for x in range(y):
    for y in range(4):
        # x + y

更多信息请见PEP-0202

- The form [... for x... for y...] nests, with the last index
      varying fastest, just like nested for loops.

【讨论】:

  • 我可以看到是这样的。我要问的是为什么会这样。将“外部”范围 (y) 放在右侧会更有意义,因为我们将 for 子句放在右侧。
  • 而且使用嵌套的for循环比使用multiple for LC更清晰
  • 除非我错过了什么,否则您的链接会再次解释是什么,而不是为什么。
【解决方案2】:

列表推导首先作为这种形式的语法糖被引入 Python:

L = []
for innerseq in seq:
    for item in innerseq:
        LOOPS
            if CONDITION:
                L.append(BODY)

这被转化为:

[BODY for innerseq in seq for item in innerseq LOOPS if CONDITION]

为了使转换更加明显,请注意for 表达式和if 条件的出现顺序与它们在正常for 循环中的顺序完全相同。这就是列表推导使用相同顺序的原因。

当您将循环重写为推导式时,唯一改变的是循环主体的位置(它移动到前面,您通常会在此处初始化空容器)。循环的其他一切都保持不变。

您喜欢的替代方案(您的“正确方式”)似乎都更令人困惑。我们要么只是颠倒循环的顺序,要么颠倒理解中every子句的顺序。即,要么:

[BODY LOOPS[::-1] for item in innerseq for innerseq in seq if CONDITION]

或者

[BODY if CONDITION LOOPS[::-1] for item in innerseq for innerseq in seq]

其中任何一个看起来都是不必要的复杂转换。

另请注意,其他语言在其列表推导中使用相同的循环顺序。这是一些 Clojure:

user=> ; using your suggested "right" order
user=> (for [x (range y) y (range 4)] (+ x y))
CompilerException java.lang.RuntimeException: Unable to resolve symbol: y in this context, compiling:(NO_SOURCE_PATH:1) 
user=> ; you need to use the same "wrong" order as Python
user=> (for [y (range 4) x (range y)] (+ x y))
(1 2 3 3 4 5)

这与 Python 相同,尽管 Clojure 将理解的“主体”放在最后。

如果有帮助,想象一下for 循环的排列方式就像汽车里程表中的数字一样。最右边的循环旋转速度最快。

【讨论】:

  • 在您的 clojure 示例中,看起来(我不知道 Clojure)两个循环都没有使用另一个循环的变量;它肯定可以以任何一种方式工作,在 Python 中也是如此。
  • 求值顺序是一样的——从左到右。我可以更改 Clojure 示例以使用已定义的变量,但这不会改变此处说明的原理。
  • 您建议将循环的顺序或每个子句的顺序颠倒 - 我会说 for 子句的顺序应该颠倒,每个条件都放在右边最外层的 for 子句是有意义的。这反映了数学中的集合生成器符号:我们为 X 中的 x 写 {P(x) 使得 Q(x)};我们可以写 {P(x, y) for x in X 这样 Q(x, y) for y in Y 这样 R(y)}。
  • Python 没有数学家谱。在大多数编程语言中,var 是按照从左到右读取代码时出现的顺序计算或定义的。在语言中存在一个特定的特殊情况,其中 vars 以与读取它们的顺序相反的顺序存在是不明智的,尤其是对于像 Python 这样的语言强调可读性和最少的惊喜。
  • 另外想象你有一个正常的循环,你正在重构为一个列表理解,反之亦然。如果循环项目被颠倒,这种转换变得非常复杂(重新洗牌)而不是仅仅将正文复制粘贴到新位置并添加一些缩进。
【解决方案3】:

这是一个很好的问题!

答案是,与大多数 Python 不同,这些嵌套推导式是按照解释器想要读取它们的方式编写的,而不是您想要编写它们的方式。

解释器从右到左阅读理解:

[(this is interpreted last) for (this is interpreted first)]

但是,每个子句都是从左到右阅读的,因此在 y in range(4) for x in range(y) 中,您必须先解释 y 是什么,然后才能将 range(y) 从中取出。

这很令人困惑,因为当您考虑 嵌套理解时,您会认为是从右到左。

【讨论】:

  • 它“从左到右读取”是不正确的。毕竟,即使在正确的用法中,yfor y in range(4) 中定义之前出现在x + y 中。
  • 是的,解释器从右到左评估理解 - 就像许多其他语言一样。
  • @DavidRobinson -- 你是对的,但这与你将得到的 WHY 问题的答案一样接近。据我所知,其他答案/cmets 也可能是“因为它就是这样”。
  • 解释器只解释从右到左的理解。
猜你喜欢
  • 2015-10-10
  • 2010-11-28
  • 2021-04-30
  • 2010-12-31
  • 1970-01-01
  • 2023-01-15
  • 2012-07-18
  • 2020-08-08
  • 1970-01-01
相关资源
最近更新 更多