词典理解中的操作顺序

2024-10-01 13:40:06 发布

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

我遇到了以下有趣的想法:

假设您有如下列表:

my_list = [['captain1', 'foo1', 'bar1', 'foobar1'], ['captain2', 'foo2', 'bar2', 'foobar2'], ...]

您想用0-index元素作为键来创建dict。一种简便的方法是:

^{pr2}$

看起来,pop在赋值给list x之前,这就是为什么'captain'没有出现在值中(它已经弹出)

现在让我们更进一步,试着得到这样的结构:

# {'captain1': {'column1': 'foo1', 'column2': 'bar1', 'column3': 'foobar1'}, ...}

对于这项任务,我写了以下内容:

my_headers = ['column1', 'column2', 'column3']
my_dict = {x.pop(0): {k: v for k, v in zip(my_headers, x)} for x in my_list}

但这会带来:

# {'captain1': {'col3': 'bar1', 'col1': 'captain1', 'col2': 'foo1'}, 'captain2': {'col3': 'bar2', 'col1': 'captain2', 'col2': 'foo2'}}

因此,本例中的pop发生在构造内部字典之后(或者至少在{}之后)。在

怎么可能呢?这是怎么回事?在

问题不在于如何去做,而在于为什么会看到这种行为。在

我使用的是Python版本3.5.1。


Tags: mypopdictlistheaderscolumn1column2foo1
3条回答

As it seems, the pop precedes the assignment of list x as the value and that is why 'captain' does not appear in the values (it is already popped)

不,事情发生的顺序无关紧要。您正在改变列表,以便在使用pop之后看到修改后的列表。请注意,一般情况下,您可能不想这样做,因为这样会破坏原始列表。即使这不重要,这一次也会成为未来不小心的陷阱。在

在这两种情况下,首先计算值侧,然后计算相应的键。只是第一种情况无关紧要,而在第二种情况下却如此。在

您可以很容易地看到:

>>> def foo(a): print("foo", a)
... 
>>> def bar(a): print("bar", a)
... 
>>> { foo(a):bar(a) for a in (1, 2, 3) }
('bar', 1)
('foo', 1)
('bar', 2)
('foo', 2)
('bar', 3)
('foo', 3)
{None: None}
>>> 

请注意,您不应该编写依赖于首先评估的值的代码:在将来的版本中,行为可能会发生变化(在某些地方,在python3.5和更高版本中,虽然事实上情况并非如此)。在

一种更简单的方法可以避免改变原始数据结构:

^{2}$

或者你的第二个例子:

my_headers = ['column1', 'column2', 'column3']
my_dict = {x[0]: {k: v for k, v in zip(my_headers, x[1:])} for x in my_list}

回答注释:zip使用原始的x,因为它是在pop之前计算的,但是它使用列表的内容来构造一个新的列表,这样以后对列表的任何更改都不会出现在结果中。第一个理解也使用原始的x作为值,但是它会对列表进行变异,这样值仍然可以看到原始列表,因此也会发生变异。在

正如我在评论中所说,这是因为在字典理解中,python首先计算值。作为一种更具python风格的方法,您可以使用解压变量来完成此任务,而不是在每次迭代中从列表中弹出:

In [32]: my_list = [['captain1', 'foo1', 'bar1', 'foobar1'], ['captain2', 'foo2', 'bar2', 'foobar2']]

In [33]: {frist: {"column{}".format(i): k for i, k in enumerate(last, 1)} for frist, *last in my_list}
Out[33]: 
{'captain2': {'column3': 'foobar2', 'column1': 'foo2', 'column2': 'bar2'},
 'captain1': {'column3': 'foobar1', 'column1': 'foo1', 'column2': 'bar1'}}

关于python在计算字典理解中的键和值时的奇怪行为,经过一些实验,我意识到这种行为在某种程度上是合理的,而不是一个bug。在

我将从以下几个方面来减少我的印象:

  1. 在赋值表达式中,python首先计算右侧。 来自文件:

    Python evaluates expressions from left to right. Notice that while evaluating an assignment, the right-hand side is evaluated before the left-hand side.

  2. 字典理解是一个表达式,它将从左到右进行计算,但是由于在python的翻译下有一个赋值。 首先计算右边的had值。在

    例如以下理解:

    {b.pop(0): b.pop(0) for _ in range(1)} 与以下代码段等效:


^{2}$

以下是一些示例:

In [12]: b = [4, 0]

# simple rule : Python evaluates expressions from left to right.
In [13]: [[b.pop(0), b.pop(0)] for _ in range(1)]
Out[13]: [[4, 0]]

In [14]: b = [4, 0]
# while evaluating an assignment (aforementioned rule 1), the right-hand side is evaluated before the left-hand side.
In [15]: {b.pop(0): b.pop(0) for _ in range(1)}
Out[15]: {0: 4}

In [16]: b = [4, 0]
# This is not a dictionary comprehension and will be evaluated left to right.
In [17]: {b.pop(0): {b.pop(0) for _ in range(1)}}
Out[17]: {4: {0}}

In [18]: b = [4, 0]
# This is not a dictionary comprehension and will be evaluated left to right.
In [19]: {b.pop(0): b.pop(0) == 0}
Out[19]: {4: True}

In [20]: b = [4, 0]
# dictionary comprehension.
In [21]: {b.pop(0): {b.pop(0) for _ in range(1)} for _ in range(1)}
Out[21]: {0: {4}}

关于事实之间的差异(或者更确切地说是抽象),字典理解是表达式,应该从左到右进行计算(基于python文档) 根据观察到的行为,我认为这实际上是python文档的问题和不成熟,而不是python代码中的bug。因为毫无例外地拥有一致的文档而更改功能是完全不合理的。在

注意:从python3.8和PEP 572开始,这一点已经改变,并且首先计算密钥。在


tl;dr Until python3.7:尽管Python首先计算值(表达式的右侧),但根据the reference manualthe grammarPEP on dict comprehensions,这在(C)Python中确实是一个bug。在

虽然这是以前的fixed for dictionary displays,其中的值在键之前被再次评估,补丁没有被修改为包含dict理解。This requirement was also mentioned by one of the core-devs in a mailing list thread discussing this same subject。在

根据参考手册,Python从左到右计算表达式和从右到左的赋值;dict理解实际上是包含表达式的表达式,不是赋值*

{expr1: expr2 for ...}

其中,根据相应的rule of the ^{},人们期望expr1: expr2的计算方式与它在显示中所做的类似。因此,这两个表达式都应该遵循定义的顺序,expr1应该在expr2之前求值(而且,如果expr2包含自己的表达式,它们也应该从左到右进行计算。)

dict comps上的PEP还指出以下内容在语义上应该是等价的:

The semantics of dict comprehensions can actually be demonstrated in stock Python 2.2, by passing a list comprehension to the built-in dictionary constructor:

>>> dict([(i, chr(65+i)) for i in range(4)])

is semantically equivalent to:

>>> {i : chr(65+i) for i in range(4)}

元组(i, chr(65+i))是否按预期从左到右求值。在

当然,将其更改为根据表达式规则的行为将在dicts的创建中造成不一致。字典理解和带有赋值的for循环会导致不同的求值顺序,但这没关系,因为它只是遵循规则。在

虽然这不是一个主要的问题,但它应该被修正(无论是评估规则,还是文档),以消除这种情况的歧义。在

*在内部,这确实会导致对dictionary对象的赋值,但这不应破坏表达式应该具有的行为。用户对表达式的行为有期望,如参考手册中所述。在


正如其他回答者所指出的那样,由于您在其中一个表达式中执行了一个变异操作,所以您会丢弃关于首先计算的内容的任何信息;使用print调用,就像邓肯所做的那样,可以揭示出所做的事情。在

帮助显示差异的函数:

^{2}$

(固定)字典显示:

>>> d = {printer(0): printer(1), printer(2): printer(3)}
0 1 2 3

(奇怪)字典理解:

>>> t = (0, 1), (2, 3)
>>> d = {printer(i):printer(j) for i,j in t}
1 0 3 2

是的,这特别适用于CPython。我不知道其他实现如何评估这个特定的情况(尽管它们都应该符合Python参考手册)

挖掘源代码总是很好的(您还可以找到描述行为的隐藏注释),所以让我们来看看文件^{}compiler_sync_comprehension_generator

case COMP_DICTCOMP:
    /* With 'd[k] = v', v is evaluated before k, so we do
       the same. */
    VISIT(c, expr, val);
    VISIT(c, expr, elt);
    ADDOP_I(c, MAP_ADD, gen_index + 1);
    break;

这似乎是一个足够好的理由,如果这样判断的话,应该被归类为文档错误。在

在我做的一个快速测试中,切换这些语句(VISIT(c, expr, elt);首先访问),同时切换相应的order in ^{}(用于dict comp):

TARGET(MAP_ADD) {
    PyObject *value = TOP();   # was key 
    PyObject *key = SECOND();  # was value
    PyObject *map;
    int err;

评估中的结果基于文档,在值之前评估密钥。(对于异步版本,这是另一个需要的开关。)


如果有人回复我,我会对这个问题发表评论并更新。

在跟踪器上创建了Issue 29652 -- Fix evaluation order of keys/values in dict comprehensions。当问题有进展时会更新问题。在

相关问题 更多 >