字典理解中的操作顺序

Ev.*_*nis 22 python dictionary python-3.x

我遇到了以下有趣的结构:

假设您有一个列表列表如下:

my_list = [['captain1', 'foo1', 'bar1', 'foobar1'], ['captain2', 'foo2', 'bar2', 'foobar2'], ...]
Run Code Online (Sandbox Code Playgroud)

并且你想用0-index元素作为键来创建一个字典.一个方便的方法是:

my_dict = {x.pop(0): x for x in my_list}
# {'captain1': ['foo1', 'bar1', 'foobar1'], ...}
Run Code Online (Sandbox Code Playgroud)

看起来,pop在将list x作为值分配之前,这就是为什么'captain'不出现在值中(它已经弹出)

现在让我们更进一步,尝试获得如下结构:

# {'captain1': {'column1': 'foo1', 'column2': 'bar1', 'column3': 'foobar1'}, ...}
Run Code Online (Sandbox Code Playgroud)

为此,我写了以下内容:

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

但是这会返回:

# {'captain1': {'col3': 'bar1', 'col1': 'captain1', 'col2': 'foo1'}, 'captain2': {'col3': 'bar2', 'col1': 'captain2', 'col2': 'foo2'}}
Run Code Online (Sandbox Code Playgroud)

所以pop在这种情况下,在构造内部字典之后(或至少在之后zip).

怎么可能?这是如何运作的?

问题不是关于如何做到这一点,而是为什么会出现这种行为.

我使用的是Python 3.5.1.

Jim*_*ard 15

tl; dr:尽管Python确实首先评估了值(表达式的右侧),根据参考手册以及 dict理解中的语法 PEP,这似乎是(C)Python中的错误.

虽然之前修复了字典显示,其中值在键之前再次进行了评估,但修补程序未修改为包含字典理解.讨论同一主题的邮件列表线程中的一个核心开发人员也提到了这个要求.

根据参考手册,Python的评估表达式由左到右由右至左的分配 ; dict-comprehension实际上是一个包含表达式的表达式,而不是赋值*:

{expr1: expr2 for ...}
Run Code Online (Sandbox Code Playgroud)

其中,根据相应的规则,grammar人们期望expr1: expr2与其在显示器中的操作类似地进行评估.因此,两个表达式都应该遵循定义的顺序,expr1应该在之前进行评估expr2(如果expr2包含它自己的表达式,它们也应该从左到右进行评估.)

dict-comps上的PEP还声明以下内容应在语义上等效:

dict理解的语义实际上可以通过将列表理解传递给内置字典构造函数来体现在Python 2.2中:

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

在语义上等同于:

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

是否(i, chr(65+i))按预期从左到右评估元组.

当然,将此更改为根据表达式规则进行操作会在创建dicts时产生不一致.字典理解和带有赋值的for循环会产生不同的评估顺序,但这很好,因为它只是遵循规则.

虽然这不是一个主要问题,但应该修复(评估规则或文档)来消除歧视情况.

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


正如其他回答者指出的那样,由于你在其中一个表达式中执行了一个变异动作,所以你要先删掉任何被评估的信息; print正如邓肯所做的那样,使用电话可以了解所做的事情.

有助于显示差异的功能:

def printer(val):
    print(val, end=' ')
    return val
Run Code Online (Sandbox Code Playgroud)

(固定)字典显示:

>>> d = {printer(0): printer(1), printer(2): printer(3)}
0 1 2 3
Run Code Online (Sandbox Code Playgroud)

(奇数)字典理解:

>>> t = (0, 1), (2, 3)
>>> d = {printer(i):printer(j) for i,j in t}
1 0 3 2
Run Code Online (Sandbox Code Playgroud)

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

挖掘源代码总是很好的(你也可以找到描述行为的隐藏注释),所以让我们看看compiler_sync_comprehension_generator文件compile.c:

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

这似乎是一个足够好的理由,如果判断为这样,则应归类为文档错误.

在我做的快速测试中,切换这些语句(VISIT(c, expr, elt);首先访问)同时切换相应的顺序MAP_ADD(用于dict-comps):

TARGET(MAP_ADD) {
    PyObject *value = TOP();   # was key 
    PyObject *key = SECOND();  # was value
    PyObject *map;
    int err;
Run Code Online (Sandbox Code Playgroud)

结果是基于文档的评估结果,在值之前评估密钥.(不是因为它们的异步版本,那是另一个需要的开关.)


我会对这个问题发表评论,并在有人回复我时更新.

创建问题29652 -在跟踪器上修复dict理解中键/值的评估顺序.将在进展时更新问题.

  • @ JimFasarakis-Hilliard:实现没有记录,但声明等同于一些示例实现的语义只会记录语义,而不是真正的实现. (2认同)

Dun*_*can 14

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

不,它发生的顺序是无关紧要的.您正在改变列表,这样您就可以在弹出窗口后看到修改后的列表.请注意,通常您可能不希望这样做,因为您将销毁原始列表.即使这次无关紧要,也是未来不知情的陷阱.

在这两种情况下,首先计算值侧,然后计算相应的键.只是在你的第一种情况下它并不重要,而它在第二种情况下.

你很容易看到这个:

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

请注意,您不应该编写依赖于首先评估的值的代码:在将来的版本中,行为可能会发生变化(在某些地方有人说在Python 3.5及更高版本中已经改变了,但事实上似乎并非如此).

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

my_dict = {x[0]: x[1:] for x in my_list}
Run Code Online (Sandbox Code Playgroud)

或者你的第二个例子:

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

要回答注释:zip使用原始文件,x因为它在之前进行了评估pop,但是它使用列表的内容来构造新列表,因此在结果中看不到对列表的任何后续更改.第一个理解也使用原始x值作为值,但它然后改变列表,因此值仍然可以看到原始列表,从而看到变异.

  • "*不,它发生的顺序是无关紧要的.你正在改变列表,这样你就可以在pop使用它之后看到修改后的列表*".如果是这样,为什么`zip`使用原始的`x`? (2认同)

Kas*_*mvd 7

正如我在评论中所说的那样,因为在词典中理解python首先评估该值.作为一种更加pythonic的方法,您可以使用解包变量来完成此任务,而不是在每次迭代中从列表中弹出:

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

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

我会在以下部分中减掉我的印象:

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

    Python从左到右评估表达式.请注意,在评估分配时,右侧在左侧之前进行评估.

  2. 字典理解是一个表达式,将从左到右进行评估,但由于在通过python进行翻译后,在引擎盖下有一个赋值.将首先评估具有权利的一方的价值.

    例如以下理解:

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


def dict_comprehension():
    the_dict = {}
    for _ in range(1):
        the_dict[b.pop(0)] = b.pop(0)
    return the_dict
Run Code Online (Sandbox Code Playgroud)

这里有些例子:

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

关于事实(或者更好地说抽象)之间的差异,字典理解是表达式,应该从左到右(基于python文档)评估观察到的行为,我认为它实际上是python文档的问题和不成熟不是python代码中的错误.因为具有一致的文档而没有任何例外,所以改变功能是不合理的.