Python中的“string”%[1, 2, 3]不会引发TypeError错误。

14

str.__mod__ 的确切行为是否已被记录在文档中?

这两行代码的效果与预期完全相同:

>>> 'My number is: %s.' % 123
'My number is: 123.'
>>> 'My list is: %s.' % [1, 2, 3]
'My list is: [1, 2, 3].'

这行也如预期一样运作:

>>> 'Not a format string' % 123
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: not all arguments converted during string formatting

但是这行代码的含义是什么,为什么它不会导致任何错误?

>>> 'Not a format string' % [1, 2, 3]
'Not a format string'

P. S.

>>> print(sys.version)
3.3.2 (default, Aug 15 2013, 23:43:52) 
[GCC 4.7.3]

2
请注意,这种行为在Python 3.8中仍然存在,似乎只有列表或字典参数无法产生错误。 - chepner
range()在Python 3.7.3中也不会产生错误,但生成器表达式会。这似乎很神秘。 - John Coleman
奇怪的是,字典、列表和范围都具有的一个神奇方法,在元组中却没有(会抛出错误)。另一方面,字典、列表和范围都实现了 '__iter__', '__len__', '__contains__', '__getitem__',但普通数字却没有——因此我怀疑观察到的行为涉及其中一个方法。 - John Coleman
@JohnColeman 请看我的回答。通常,所有可以被用作“映射”的参数并且可以被索引的参数都不会引发错误,但是 tuple 显式地被排除在外,可能是因为它们被用于可变参数拆包。 - Jan Christoph Terasa
@JanChristophTerasa 这是一个很好的答案(+1)。作为一个快速测试,我刚刚编写了一个只有一行定义def __getitem__(self): pass的类,并且这个类的对象可以像你的答案所建议的那样工作。在这里,元组被自动解包,因此被排除在外是有道理的。 - John Coleman
2个回答

9

我认为可以在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,因此不会进入错误情况。 dictlist都可以用作这样的映射,因此在用作参数时不会引发错误。tuple在第14976行的检查中被明确排除,可能是因为它用于将可变参数传递给格式化程序。
至于这种行为是否有意,或者为什么是有意的,对我来说还不清楚,因为源代码中的部分没有注释。
基于此,我们可以尝试:
assert 'foo' % [1, 2] == 'foo'
assert 'foo' % {3: 4} == 'foo'
class A:
    pass
assert 'foo' % A() == 'foo'
# TypeError: not all arguments converted during string formatting
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可以告诉我们为什么这是预期行为。


5
当最新printf样式格式被添加时,似乎在百分号格式化中出现了许多小问题。今天(版本3.8),这里已有记录,但早在版本3.3就已经提到
这里描述的格式化操作展示了各种怪异行为,导致许多常见错误(例如无法正确显示元组和字典)。使用更新的格式化字符串文字、str.format()接口或模板字符串可以避免这些错误。每个替代方案都提供了自己的简单性、灵活性和/或可扩展性的权衡和优点。
在这种情况下,Python看到了一个非元组值,并且在%的右侧看到了一个__getitem__方法,因此假定必须执行format_map。通常使用dict完成此操作,但确实可以使用任何具有__getitem__方法的对象来完成此操作。
特别地,format_map允许忽略未使用的键,因为您通常不会迭代映射项以访问它们。
>>> "Include those items: %(foo)s %(bar)s" % {"foo": 1, "bar": 2, "ignored": 3}
'Include those items: 1 2'

你的示例使用了该特性,即忽略容器中的所有键。
>>> "Include no items:" % {"foo": 1, "bar": 2}
'Include no items:'

如果您需要进一步证明,可以查看在使用list作为右侧时会发生什么。
>>> lst = ["foo", "bar", "baz"]
>>> "Include those items: %(0)s, %(2)s" % lst
TypeError: list indices must be integers or slices, not str

Python试图获取lst["0"],不幸的是没有办法指定"0"应该转换为int,所以使用%语法注定会失败。
旧版本
值得一提的是,这似乎是在Python 3.0之前出现的一个怪癖,因为我在尽可能远的版本中得到了相同的行为,尽管文档仅开始在3.3版本中提到它。
Python 3.0.1+ (unknown, May  5 2020, 09:41:19) 
[GCC 9.2.0] on linux4
Type "help", "copyright", "credits" or "license" for more information.
>>> 'Not a format string' % [1, 2, 3]
'Not a format string'

这个问题最初是在2013年提出的,比f-strings添加的时间早多了。F-strings可能已经被添加以解决这个问题,但它们并不是问题的根源。 - chepner
虽然format_map在Python 3.2中已经被引入,但挖掘一个Python 3.0或Python 3.1解释器进行比较仍然很有趣。 - chepner
@chepner 我在答案中添加了一条注释。它实际上是指printf风格的格式化,而不仅仅是f-strings,并且在3.3版本的文档中已经存在:https://docs.python.org/3.3/library/stdtypes.html#printf-style-string-formatting - Olivier Melançon
我在Python 2.7中看到了相同的行为。 - L3viathan
@L3viathan,Python 2.7.x 中使用的逻辑与我在 Py 3.x 中发现的逻辑相同(链接:https://dev59.com/eGMk5IYBdhLWcg3w0hM-#61613669),分别用于“strings”和“unicode”。 - Jan Christoph Terasa
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接