我认为可以在CPython源代码中找到相关的代码,我使用了git v3.8.2
:
在这个函数中
PyObject *
PyUnicode_Format(PyObject *format, PyObject *args)
在Objects/unicodeobject.c
文件的第14944行,有以下代码:
Objects/unicodeobject.c
,第15008行。
if (ctx.argidx < ctx.arglen && !ctx.dict) {
PyErr_SetString(PyExc_TypeError,
"not all arguments converted during string formatting");
goto onError;
}
如果
arglen
不匹配,则会出现错误,但如果
ctx.dict
为"true",则不会出现错误。什么情况下会出现这种情况?
Objects/unicodeobject.c
,第14976行。
if (PyMapping_Check(args) && !PyTuple_Check(args) && !PyUnicode_Check(args))
ctx.dict = args
else
ctx.dict = NULL
好的,PyMapping_Check
检查传递的args
,如果为"true",且我们没有元组或Unicode字符串,则设置ctx.dict = args
。
PyMapping_Check
是做什么用的?
Objects/abstract.c
,第2110行。
int
PyMapping_Check(PyObject *o)
{
return o && o->ob_type->tp_as_mapping &&
o->ob_type->tp_as_mapping->mp_subscript;
}
根据我的理解,如果对象可用作"映射",并且可以进行索引/下标操作,则会返回
1
。在这种情况下,
ctx.dict
的值将被设置为
args
,即
!0
,因此不会进入错误情况。
dict
和
list
都可以用作这样的映射,因此在用作参数时不会引发错误。
tuple
在第14976行的检查中被明确排除,可能是因为它用于将可变参数传递给格式化程序。
至于这种行为是否有意,或者为什么是有意的,对我来说还不清楚,因为源代码中的部分没有注释。
基于此,我们可以尝试:
assert 'foo' % [1, 2] == 'foo'
assert 'foo' % {3: 4} == 'foo'
class A:
pass
assert 'foo' % A() == 'foo'
class B:
def __getitem__(self):
pass
assert 'foo' % B() == 'foo'
因此,只有定义了
__getitem__
方法的对象才足以不触发错误。
编辑:在OP中引用的v3.3.2
中,同一文件中的第13922、13459和1918行是有问题的,逻辑看起来相同。
编辑2:在v3.0
中,检查在Objects/unicodeobject.c
的第8841和9226行进行,在Unicode格式化代码中没有使用Objects/abstract.c
中的PyMapping_Check
函数。
编辑3:根据一些二分和git blame,核心逻辑(关于ASCII字符串,而不是unicode字符串)可以追溯到Python 1.2,并由GvR本人在25年前实现:
commit caeaafccf7343497cc654943db09c163e320316d
Author: Guido van Rossum <guido@python.org>
Date: Mon Feb 27 10:13:23 1995 +0000
don't complain about too many args if arg is a dict
diff --git a/Objects/stringobject.c b/Objects/stringobject.c
index 7df894e12c..cb76d77f68 100644
--- a/Objects/stringobject.c
+++ b/Objects/stringobject.c
@@ -921,7 +921,7 @@ formatstring(format, args)
XDECREF(temp);
} /* '%' */
} /* until end */
- if (argidx < arglen) {
+ if (argidx < arglen && !dict) {
err_setstr(TypeError, "not all arguments converted");
goto error;
}
很可能GvR可以告诉我们为什么这是预期行为。
range()
在Python 3.7.3中也不会产生错误,但生成器表达式会。这似乎很神秘。 - John Coleman'__iter__', '__len__', '__contains__', '__getitem__'
,但普通数字却没有——因此我怀疑观察到的行为涉及其中一个方法。 - John Colemantuple
显式地被排除在外,可能是因为它们被用于可变参数拆包。 - Jan Christoph Terasadef __getitem__(self): pass
的类,并且这个类的对象可以像你的答案所建议的那样工作。在这里,元组被自动解包,因此被排除在外是有道理的。 - John Coleman