2016-03-03 77 views
16

据推测,dict_keys应该表现为类似集合的对象,但它们缺少方法,减法行为似乎有分歧。为什么字典键支持列表减法而不是元组减法?

>>> d = {0: 'zero', 1: 'one', 2: 'two', 3: 'three'} 
>>> d.keys() - [0, 2] 
{1, 3} 
>>> d.keys() - (0, 2) 
TypeError: 'int' object is not iterable 

为什么dict_keys类会尝试迭代整数?这不违反鸭子打字吗?


>>> dict.fromkeys(['0', '1', '01']).keys() - ('01',) 
{'01'} 
>>> dict.fromkeys(['0', '1', '01']).keys() - ['01',] 
{'1', '0'} 
+1

元组是不可变的,因此可能是字典本身的关键字,在这种情况下,这将是不明确的 - 如果没有语法上的话,至少在程序员的脑海中是这样。 – L3viathan

+1

@ L3viathan我不相信,因为'dict.fromkeys('0123')。keys() - '02''仍然有效 – wim

+0

是的,好点。 – L3viathan

回答

17

这看起来是一个错误。 The implementation is to convert the dict_keys to a set, then call .difference_update(arg) on it.

看起来他们滥用_PyObject_CallMethodId(的PyObject_CallMethod优化的变体),通过传递的只是"O"格式字符串。 Thing is, PyObject_CallMethod and friends are documented to require a Py_BuildValue format string that "should produce a tuple"。使用多个格式代码时,它会自动将值包含在tuple中,但只有一个格式代码,它不会tuple,它只是创建值(在这种情况下,因为它已经是PyObject*,它所做的只是增加参考计数)。

虽然我还没有追查它可能会做这个,我怀疑的地方在它的识别不产生tupleCallMethod电话和包裹它们的内部,使一个元素tuple所以调用的函数实际上可以以预期格式接收参数。当减去tuple时,它已经是tuple,并且此修复代码从未激活;当通过list时,它成为包含list的一个元素tuple

difference_update需要可变参数(好像它被宣布为def difference_update(self, *args))。因此,当它收到展开的tuple时,它认为它应该从tuple中的每个条目中减去元素,而不是将所述条目视为自己减去值的值。为了说明这一点,当你这样做:

mydict.keys() - (1, 2) 

的错误导致它做的(大约):

result = set(mydict) 
# We've got a tuple to pass, so all's well... 
result.difference_update(*(1, 2)) # Unpack behaves like difference_update(1, 2) 
# OH NO! 

虽然:

mydict.keys() - [1, 2] 

做:

result = set(mydict) 
# [1, 2] isn't a tuple, so wrap 
result.difference_update(*([1, 2],)) # Behaves like difference_update([1, 2]) 
# All's well 

这就是为什么tuplestr件作品(错误地),- ('abc', '123')就是执行呼叫相当于:

result.difference_update(*('abc', '123')) 
# or without unpacking: 
result.difference_update('abc', '123') 

而且由于str s为他们的角色的iterables,它只是轻率地删除条目'a''b''c'等代替'abc''123'像你期望的那样。

基本上,这是一个错误,并且(当我有机会的时候),我会将它提交给CPython人员。

正确的行为或许应该已经调用(假设这Id变种存在此API):

_PyObject_CallMethodObjArgsId(result, &PyId_difference_update, other, NULL); 

这不会有问题的包装在所有的,并会跑得更快引导;最小的变化是将格式字符串更改为"(O)"以强制tuple即使对于单个项目也是如此,但由于格式字符串没有收益,因此_PyObject_CallMethodObjArgsId更好。

+3

[问题#26478打开](https://bugs.python.org/issue26478)。 – ShadowRanger

+1

现在它已经修复了(尽管你必须等待3.4/3.5的下一个次要版本或3.6.0版本才能将修补程序合并到主版本中)。 – ShadowRanger