据推测,dict_keys应该表现为类似集合的对象,但它们缺少difference
方法,并且减法行为似乎有所不同。
>>> 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'}
答案 0 :(得分: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*
,它只是递增引用计数。)
虽然我没有追踪它可能会在哪里做这个,但我怀疑在内部的某个地方,它正在识别CallMethod
不会产生tuple
的调用并将它们包装成一个元素tuple
所以被调用的函数实际上可以接收预期格式的参数。减去tuple
时,它已经是tuple
,此修正代码永远不会激活;传递list
时,它会成为包含tuple
的一个元素list
。
difference_update
接受varargs(就好像它被声明为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
这就是tuple
str
工作(错误)的原因,- ('abc', '123')
正在执行相当于以下的调用:
result.difference_update(*('abc', '123'))
# or without unpacking:
result.difference_update('abc', '123')
由于str
是其字符的可迭代,因此它只是轻松删除'a'
,'b'
,'c'
等的条目,而不是'abc'
和你期望的'123'
一样。
基本上,这是一个错误(当我有机会的时候),我会把它提交给CPython人员。
可能应该调用正确的行为(假设此API存在此Id
变体):
_PyObject_CallMethodObjArgsId(result, &PyId_difference_update, other, NULL);
根本没有包装问题,并且可以更快地启动;最小的变化是将格式字符串更改为"(O)"
以强制tuple
创建,即使对于单个项目也是如此,但由于格式字符串没有任何效果,_PyObject_CallMethodObjArgsId
更好。