我遇到了以下有趣的构造:
假设您有一个列表列表如下:
my_list = [['captain1', 'foo1', 'bar1', 'foobar1'], ['captain2', 'foo2', 'bar2', 'foobar2'], ...]
并且你想用0
- 索引元素作为键来创建一个字典。一个方便的方法是:
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。
答案 0 :(得分:15)
tl; dr :即使Python 首先评估值(表达式的右侧),这确实会出现根据{{3}}和the reference manual以及the grammar,在(C)Python中成为中的错误。
虽然之前是PEP on dict comprehensions,其中值在键之前再次进行了评估,补丁未被修改以包含dict-comprehensions。 fixed for dictionary displays
根据参考手册,Python从左到右评估表达式,从右到左评估分配; dict-comprehension实际上是一个包含表达式的表达式,不是赋值 * :
{expr1: expr2 for ...}
其中,根据相应的This requirement was also mentioned by one of the core-devs in a mailing list thread discussing this same subject,可以期望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))
按预期从左到右进行评估。
当根据表达式规则更改此行为时,会在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
是的,这适用于C
Python。我不知道其他实现如何评估这个特定情况(尽管它们都应该符合Python参考手册。)
挖掘源代码总是很好(而且你也发现了描述行为的隐藏评论),所以让我们看一下文件rule of the grammar
的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);
首先访问),同时切换相应的compile.c
(用于dict-comps):
TARGET(MAP_ADD) {
PyObject *value = TOP(); # was key
PyObject *key = SECOND(); # was value
PyObject *map;
int err;
导致基于文档的评估结果,在值之前评估密钥。 (不是因为它们的异步版本,那是另一个需要的开关。)
我会就此问题发表评论,并在有人回复我时更新。
在跟踪器上创建order in MAP_ADD
。将在进展时更新问题。
答案 1 :(得分:14)
看起来,pop在list x的赋值之前作为值 这就是“队长”没有出现在价值观中的原因(已经是 弹出)
不,它发生的顺序无关紧要。您正在改变列表,这样您就可以在弹出窗口后看到修改后的列表。请注意,通常您可能不希望这样做,因为您将销毁原始列表。即使这次无关紧要,也是未来一个不知情的陷阱。
在这两种情况下,首先计算值侧,然后计算相应的键。只是在你的第一种情况下它并不重要,而它在第二种情况下会发生。
你可以很容易地看到这一点:
>>> 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}
>>>
请注意,您不应该编写依赖于首先评估的值的代码:在将来的版本中行为可能会发生变化(在某些地方有人说在Python 3.5及更高版本中已经改变了,但事实上它似乎不是情况)。
一种更简单的方法,可以避免改变原始数据结构:
my_dict = {x[0]: x[1:] for x in my_list}
或者你的第二个例子:
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
作为值,但然后它会改变列表,以便值仍然可以看到原始列表,从而看到变异。
答案 2 :(得分: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'}}
关于python在评估字典理解中的键和值时的奇怪行为,经过一些实验,我意识到这种行为在某种程度上是合理的而不是一个bug。
我会在以下部分中消除我的印象:
在赋值表达式中,python首先计算右侧。 来自doc:
Python从左到右评估表达式。请注意,在评估分配时,右侧会在左侧之前进行评估。
字典理解是一个表达式,将从左到右进行评估,但由于在通过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
以下是一些例子:
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代码中的错误。因为在没有任何例外的情况下拥有一致的文档,所以改变功能是完全不合理的。
答案 3 :(得分:4)
实际上,您的观察不需要特殊的操作顺序。原因是x.pop(0)
修改了对象x
。因此,在这种情况下,无论是在密钥(x
)之前还是之后评估值(x.pop(0)
)都无关紧要。
无论如何,我不认为python语言规范规定了某种操作顺序,这意味着你不应该依赖任何特定的顺序。
实际上标准实现发生在评估密钥之前评估该值,但标准中没有任何地方说明这一点。唯一的保证是键值对以迭代顺序进行评估,并按顺序插入。