python - 字典理解中的操作顺序

标签 python python-3.x dictionary

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

假设您有如下列表:

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

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

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

看起来,pop 在分配列表 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 发生在内部字典构建之后(或至少在 zip 之后)。

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

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

我使用的是 Python 版本 3.5.1。

最佳答案

注意:从 Python 3.8 和 PEP 572 开始,这已更改,并且首先评估键。


tl;dr 直到 Python 3.7:尽管 Python 确实首先评估值(表达式的右侧)根据 the reference manual,这似乎是 (C)Python 中的一个错误the grammarPEP on dict comprehensions .

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

根据引用手册,Python 计算表达式从左到右赋值从右到左; dict-comprehension 实际上是一个包含表达式的表达式,不是赋值*:

{expr1: expr2 for ...}

其中,根据rule of the grammar对应人们会期望 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)) 是否按预期从左到右进行评估。

当然,将其更改为根据表达式规则执行会在创建 dict 时产生不一致。字典推导和带有赋值的 for 循环会导致不同的评估顺序,但这很好,因为它只是遵循规则。

虽然这不是主要问题,但应该修复它(评估规则或文档)以消除歧义。

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


正如其他回答者指出的那样,由于您在其中一个表达式中执行了变异操作,因此您丢弃了有关首先评估什么的任何信息;正如 Duncan 所做的那样,使用 print 调用可以阐明所做的工作。

帮助显示差异的功能:

def printer(val):
    print(val, end=' ')
    return val

(固定)字典显示:

>>> 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 引用手册。)

挖掘源代码总是不错的(而且您还可以找到描述行为的隐藏注释),所以让我们看看文件 compile.ccompiler_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 MAP_ADD (用于 dict-comps):

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在跟踪器上。将在问题取得进展时更新问题。

关于python - 字典理解中的操作顺序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42201932/

相关文章:

Python:__add__ 和 +, float 和整数的不同行为

python - 使用 python 替换文件中最后 2 次出现的特定单词

http - 这些 python Web 服务器请求之间有什么区别?

python-3.x - 使用 Scikit-Learn 在 Python 中绘制多项式回归

matlab - 如何创建使用数组作为键的 Matlab 字典

swift - 在 Swift 中使用 reduce 构建字典

c# - 在 C# 中,如何从字典中获取键列表?

python - 为什么 Flask-SocketIO 有 `http` 协议(protocol)而不是 `ws` ?

python - 创建新的 iPython Notebook 时内核死机。

python - 在 Python 中设置 celery 任务后端的麻烦