【问题标题】:Why are arbitrary target expressions allowed in for-loops?为什么 for 循环中允许任意目标表达式?
【发布时间】:2017-11-03 05:16:12
【问题描述】:

我不小心写了一些这样的代码:

foo = [42]
k = {'c': 'd'}

for k['z'] in foo:  # Huh??
    print k

但令我惊讶的是,这不是语法错误。相反,它会打印{'c': 'd', 'z': 42}

我的猜测是该代码按字面意思翻译为:

i = iter(foo)
while True:
    try:
        k['z'] = i.next()  # literally translated to assignment; modifies k!
        print k
    except StopIteration:
        break

但是...为什么语言允许这样做?我希望在for-stmt's target expression 中只允许单个标识符和标识符元组。在任何情况下这实际上是有用的,而不仅仅是一个奇怪的问题?

【问题讨论】:

  • 允许它不一定有用。字典中的键值是有效的标识符。由于命名空间本身就是字典,因此更改它需要更改大量 Python 内部结构。并且修复一些只有在您编写不会导致错误的意外代码时才会出现的问题。
  • @AlanLeuthard 我明白你在说什么(你的回答很好地充实了它)但更准确地说,identifier 只能是字母和数字的序列,即实际变量name 而不是其他可分配的表达式,例如 subscription
  • 是的。应该说目标而不是标识符
  • 哦该死,对不起,这应该是相反的 :-) 不应该在困的时候使用锤子。

标签: python for-loop language-lawyer


【解决方案1】:

for 循环遵循标准的赋值规则,因此在普通赋值的 LHS 上有效的内容应该适用于 for

使用标准将每个项目依次分配到目标列表 作业规则

for 构造简单地调用了分配给目标的底层机制,在您的示例代码中是STORE_SUBSCR

>>> foo = [42]
>>> k = {'c': 'd'}
>>> dis.dis('for k["e"] in foo: pass')
  1           0 SETUP_LOOP              16 (to 18)
              2 LOAD_NAME                0 (foo)
              4 GET_ITER
        >>    6 FOR_ITER                 8 (to 16)
              8 LOAD_NAME                1 (k)
             10 LOAD_CONST               0 ('e')
             12 STORE_SUBSCR <--------------------
             14 JUMP_ABSOLUTE            6
        >>   16 POP_BLOCK
        >>   18 LOAD_CONST               1 (None)
             20 RETURN_VALUE

但令我惊讶的是,这不是语法错误

显然,任何在常规作业中有效的方法,例如:

完整切片分配

>>> for [][:] in []:
...    pass
... 
>>>

列表订阅

>>> for [2][0] in [42]:
...    pass
... 
>>> 

字典订阅等将是有效的候选目标,唯一的例外是链式分配;不过,我暗中认为可以编造一些肮脏的语法来执行链接。


我希望只有单个标识符和标识符元组

我想不出一个将字典键作为目标的好的用例。此外,在循环体中进行字典键分配比在for 子句中使用它作为目标更具可读性。

然而,在常规赋值中非常有用的扩展解包(Python 3)在 for 循环中也同样方便:

>>> lst = [[1, '', '', 3], [3, '', '', 6]]
>>> for x, *y, z in lst:
...    print(x,y,z)
... 
1 ['', ''] 3
3 ['', ''] 6

这里也召唤了相应的分配给不同目标的机制;多个STORE_NAMEs:

>>> dis.dis('for x, *y, z in lst: pass')
  1           0 SETUP_LOOP              20 (to 22)
              2 LOAD_NAME                0 (lst)
              4 GET_ITER
        >>    6 FOR_ITER                12 (to 20)
              8 EXTENDED_ARG             1
             10 UNPACK_EX              257
             12 STORE_NAME               1 (x) <-----
             14 STORE_NAME               2 (y) <-----
             16 STORE_NAME               3 (z) <-----
             18 JUMP_ABSOLUTE            6
        >>   20 POP_BLOCK
        >>   22 LOAD_CONST               0 (None)
             24 RETURN_VALUE

表明for 几乎不是连续执行的简单赋值语句。

【讨论】:

  • 鉴于链式分配将是 (x = (y = v)) 而不是 (x = y) = v,我怀疑你能想出让它工作的语法
【解决方案2】:

下面的代码会有意义,对吧?

foo = [42]
for x in foo:
    print x

for 循环将遍历列表 foo 并依次将每个对象分配给当前命名空间中的名称 x。结果将是一次迭代和一次打印42

您的代码中的xk['z']k['z'] 是有效的存储名称。就像我的示例中的 x 一样,它还不存在。实际上,它是全局命名空间中的k.z。该循环创建k.zk['z'] 并将它在foo 中找到的值分配给它,就像在我的示例中创建x 并为其分配值一样。如果你在 foo 中有更多的值...

foo = [42, 51, "bill", "ted"]
k = {'c': 'd'}
for k['z'] in foo:
    print k

会导致:

{'c': 'd', 'z': 42}
{'c': 'd', 'z': 51}
{'c': 'd', 'z': 'bill'}
{'c': 'd', 'z': 'ted'}

您编写了完全有效的意外代码。这甚至不是奇怪的代码。您通常不会将字典条目视为变量。

即使代码不奇怪,允许这样的赋值又有什么用呢?

key_list = ['home', 'car', 'bike', 'locker']
loc_list = ['under couch', 'on counter', 'in garage', 'in locker'] 
chain = {}
for index, chain[key_list[index]] in enumerate(loc_list):
    pass

可能不是最好的方法,而是将两个长度相等的列表放在一个字典中。我确信还有其他一些更有经验的程序员在 for 循环中使用字典键分配。也许……

【讨论】:

  • 这个解释比其他解释清楚得多。干得好!
  • 虽然您的第一个代码示例并没有超出我在问题中的“猜测”,但这个解释非常清楚,我相信它对未来的读者会有用......如果有人碰巧又写了这个:)谢谢!
  • 虽然dict(zip(key_list, loc_list)) 可能比for 循环的滥用更容易理解。
  • “实际上,k.z 在全局命名空间中”,这是为什么呢?
  • 实际上反过来效果更好。您看到的每个名称基本上都是 namespace.name,相当于 namespace['name']。每个子名称都是 namespace.name.subname 或 namespace['name']['subname']。换句话说,每个变量名都是一个字典键。哈希表用于在存储值的内存中查找引用。
【解决方案3】:

每个名称只是一个字典键*。

for x in blah:

正是

for vars()['x'] in blah:

*(尽管该字典不需要实现为实际的 dict 对象,以防某些优化,例如在函数范围内)。

【讨论】:

  • 这两个代码示例并不“完全”相同,因为第一个在函数内部工作,而第二个不工作,但您的主要观点是。
  • 也许我的英语有问题。我没有说,也不想说这些代码示例完全相同。 (当然,因为您可能已经看到我在下面包含了脚注。)我的意思是,“分配给名称的精确语义只是某些字典上的一个集合”,尽管该字典不需要实现为真正的 Python dict 对象。
  • 是的,很公平,作为一个例子,这完全有道理。我刚刚看到太多人对修改locals()(或vars())返回的字典在函数内部不起作用这一事实感到困惑,所以我希望尽可能明确。
【解决方案4】:

在什么情况下这真的有用吗?

确实如此。曾经想摆脱itertools.combinations

def combinations (pool, repeat):        
    def combinations_recurse (acc, pool, index = 0):
        if index < len(acc):
            for acc[index] in pool:
                yield from combinations_recurse(acc, pool, index + 1)
        else:
            yield acc

    yield from combinations_recurse([pool[0]] * repeat, pool)

for comb in combinations([0, 1], 3):
    print(comb)

【讨论】:

    猜你喜欢
    • 2018-01-16
    • 2014-05-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-05
    • 1970-01-01
    • 2014-05-13
    • 2021-10-23
    相关资源
    最近更新 更多