![d68657eda14b279bbad0d6e5998729b6.png](https://i-blog.csdnimg.cn/blog_migrate/356b97a88b7f0824085c391bafd713a7.jpeg)
某一天吹水的时候,吹着吹着就吹到了一下这么一个案例。
import timeit
def a():
"%s, %s" % (1, 2)
def b():
"%s, %d" % (1, 2)
def c():
"%d, %d" % (1, 2)
t = timeit.timeit(stmt="a()", setup="from __main__ import a", number=1000000)
t2 = timeit.timeit(stmt="b()", setup="from __main__ import b", number=1000000)
t3 = timeit.timeit(stmt="c()", setup="from __main__ import c", number=1000000)
print "time of a", t
print "time of b", t2
print "time of c", t3
# time of a 0.178924037995
# time of b 0.420981640254
# time of c 0.651199530325
哇,这有点反直觉。输入是由两个int组成的tuple。而序列化字符串的时候,指明了类型%d竟然比不指明类型直接%s来的慢??
那这必需跟Python源码的实现逻辑有关了。以此为契机,去研究一下吧。
首先看看字节码。
============ a ============
5 0 LOAD_CONST 1 ('%s, %s')
3 LOAD_CONST 4 ((1, 2))
6 BINARY_MODULO
7 POP_TOP
8 LOAD_CONST 0 (None)
11 RETURN_VALUE
None
============ b ============
8 0 LOAD_CONST 1 ('%s, %d')
3 LOAD_CONST 4 ((1, 2))
6 BINARY_MODULO
7 POP_TOP
8 LOAD_CONST 0 (None)
11 RETURN_VALUE
None
============ c ============
11 0 LOAD_CONST 1 ('%d, %d')
3 LOAD_CONST 4 ((1, 2))
6 BINARY_MODULO
7 POP_TOP
8 LOAD_CONST 0 (None)
11 RETURN_VALUE
三个函数的字节码都是一样的。
简要看看一些熟悉的字节码。首先两个LOAD_CONST就是把字符串和(1,2)这两个变量压进栈内带后续字节码使用。
此时的栈:
(1,2)
-----
"%s,%s"
BINARY_MODULO还不知道是什么,但看着明显就是这个字节码实现的字符串序列化工作。它会取栈内两个变量进行操作,完成后也理应会把生成的字符串压到栈顶。
此时的栈
"1,2"
由于我们没有把这个值引用下来。因此函数结束时会POP_TOP,就是把BINARY_MODULO的结果直接弹出,不再需要。然后再把None元素Load进来,当做返回值返回。
那么显然,造成速度差别的就是BINARY_MODULO这一步了。
BINARY_MODULO研究
研究Python字节码的入口必是ceval.c。我们在这里找到BINARY_MODULO的具体实现。
w = POP();
v = TOP();
if (PyString_CheckExact(v)
&& (!PyString_Check(w) || PyString_CheckExact(w))) {
/* fast path; string formatting, but not if the RHS is a str subclass
(see issue28598) */
x = PyString_Format(v, w);
} else {
x = PyNumber_Remainder(v, w);
}
Py_DECREF(v);
Py_DECREF(w);
SET_TOP(x);
if (x != NULL) DISPATCH();
一行一行看。
w,v分别是获得栈顶元素。按照顺序,分别为w=(1,2),v="%s,%s"。指的注意的是,v用的是TOP(),即"%s,%s"还保留在栈顶。
下面我们来看第一个if。如果v是一个字符串,但w不是string的子类,就直接进入PyString_Format。这里我们研究的对象明显符合。好的。直接进去这个函数进行分析。顺带说一下。PyNumber_Remainder就是整数取余运算。回想我们的python代码。其实就是v%w的操作,这么一看,绝大多数人看到%符都知道这是取余操作,包括字节码的取名都是取余的意思呀!!。只不过刚好有字符串这个拼接特例而已。
PyString_Format
研究PyString_Fromat是个体力活,毕竟各种各样的字符串拼接逻辑都与此有关。接下来废话不多说,赶紧开始吧!
由于代码很长。我直接先来一发总结,这样看代码会清晰多。
之所以%d会比%s慢,是因为格式化字符串的时候,有很多选项只对%d(%x,%f同理)生效。而在Python源码里,无论你是否注明了这些选项,它都一视同仁进行一次专门对数字类型的格式化处理。由于我们经常只是直接使用%d和%s而忽略他们的一些特殊选项,导致我们的直观感受就是%d和%s应该差不多。
那么都有哪些操作呢?
![5394aa168a7573dec06a240ccfa986b7.png](https://i-blog.csdnimg.cn/blog_migrate/88e4c7511b2572d4d71262350e0728a9.jpeg)
可以这么理解,上述的功能描述里,带“数字”字眼的,都只对整数生效。我们来举个例子。
>>> "%0+5d" % 5
'+0005'
>>> "%0+5s" % 5
' 5'
看来,对于%s的情况,填充0和整数显示+这两个选项都没有用。只有宽度为5的选项生效了。
好的,了解了这个之后,上源码就舒服多了。
PyObject *
PyString_Format(PyObject *format, PyObject *args)
{
/* 省略一些声明和检查 */
char *fmt, *res;
fmt = PyString_AS_STRING(format); // 格式字符串指针
fmtcnt = PyString_GET_SIZE(format); // 格式字符串的长度
Py_ssize_t reslen, rescnt, fmtcnt;
reslen = rescnt = fmtcnt + 100;
result = PyString_FromStringAndSize((char *)NULL, reslen);
/* 默认会定义一个比格式字符串长100个字节的字符串作为返回字符串 */
res = PyString_AsString(result);
while (--fmtcnt >= 0) {
// 这里很简单,就是对格式字符串一个一个遍历,不是%就直接添加到res中
if (*fmt != '%') {
if (--rescnt < 0) {
rescnt = fmtcnt + 100;
reslen += rescnt;
if (_PyString_Resize(&result, reslen))
return NULL;
res = PyString_AS_STRING(result)
+ reslen - rescnt;
--rescnt;
}
*res++ = *fmt++;
}
else {
/* 上面的逻辑都挺清晰。接下来讲述一下遇到在格式字符串里遇到%后会做的事情 */
/* 省略一堆声明 */
if (*fmt == '(') {
/* 这里是用来换参数集合的,比如下面这种情况,不分析源码了。
"%(key2)s, %(key1)s" % {"key1":1, "key2":2} -> "2,1"
*/
}
/* 这几个格式化选项是可以连着的, 比如"%-+ #0s",检测到该项会把flag设置一下
供等一下用 */
while (--fmtcnt >= 0) {
switch (c = *fmt++) {
case '-': flags |= F_LJUST; continue;
case '+': flags |= F_SIGN; continue;
case ' ': flags |= F_BLANK; continue;
case '#': flags |= F_ALT; continue;
case '0': flags |= F_ZERO; continue;
}
break;
}
if (c == '*') {
// 有星号表示会用格式化参数的第一个来作为格式化宽度值,比如"%*s"%(5,1)
// 最后得到的宽度值存在width中
}
else if (c >= 0 && isdigit(c)) {
// 如果找到整数值,表示用该值来控制宽度,比如%5s"% 1
// 最后得到的宽度值存在width中
}
if (c == '.') {
// 遇到.号,就往后检查数字,表示小数点精度。最后得到的值会存在
// prec 变量当中
}
if (c != '%') {
// 如果%后慢没有跟%,那么就取参数列表中的下一项,准备格式化
v = getnextarg(args, arglen, &argidx);
if (v == NULL)
goto error;
}
switch (c) {
// -------------- 接下来这一块是跟 %s,%%,%r相关的
// -------------- 它们其实都是直接字符串操作,所以很快
// -------------- 格式化的套路就是把格式化的内容塞到一个叫pbuf的东西
// -------------- 最后再把pbuf的内存内容直接拷贝到经过一些预处理的res中
case '%':
pbuf = "%";
len = 1;
break;
case 's':
// %s 直接调用参数v的tp_str 或 tp_repr 或 tp_name获得字符串
temp = _PyObject_Str(v);
// 这里要注意一下,这一部后面没有break!!所以直接调到case 'r'中
/* Fall through */
case 'r':
// %r 就是限制了使用tp_repr了
if (c == 'r')
temp = PyObject_Repr(v);
if (temp == NULL)
goto error;
if (!PyString_Check(temp)) {
PyErr_SetString(PyExc_TypeError,
"%s argument has non-string str()");
Py_DECREF(temp);
goto error;
}
// 把temp值塞到pbuf中
pbuf = PyString_AS_STRING(temp);
len = PyString_GET_SIZE(temp);
if (prec >= 0 && len > prec)
len = prec;
break;
// -------------------- 这里开始是整数格式化操作
case 'i':
case 'd':
case 'u':
case 'o':
case 'x':
case 'X':
if (c == 'i')
c = 'd';
isnumok = 0;
// 拿到参数值,并转化为PyIntObject 或 PyLongObject 存到iobj中
if (PyNumber_Check(v)) {
PyObject *iobj=NULL;
if (_PyAnyInt_Check(v)) {
iobj = v;
Py_INCREF(iobj);
}
else {
iobj = PyNumber_Int(v);
if (iobj==NULL) {
PyErr_Clear();
iobj = PyNumber_Long(v);
}
}
if (iobj!=NULL) {
if (PyInt_Check(iobj)) {
isnumok = 1;
pbuf = formatbuf;
// 把之间检查到的所有格式化选项,比如flags和prec,
// 都跟iobj一起扔进去处理,并把处理结果塞在pbuf中,
// 同时返回结果的长度
len = formatint(pbuf,
sizeof(formatbuf),
flags, prec, c, iobj);
Py_DECREF(iobj);
if (len < 0)
goto error;
sign = 1;
}
else if (PyLong_Check(iobj)) {
int ilen;
isnumok = 1;
temp = _PyString_FormatLong(iobj, flags,
prec, c, &pbuf, &ilen);
Py_DECREF(iobj);
len = ilen;
if (!temp)
goto error;
sign = 1;
}
else {
Py_DECREF(iobj);
}
}
}
if (!isnumok) {
PyErr_Format(PyExc_TypeError,
"%%%c format: a number is required, "
"not %.200s", c, Py_TYPE(v)->tp_name);
goto error;
}
if (flags & F_ZERO)
fill = '0';
break;
/* 下面省略其他情况,比如%f和%c等,离题了。。 */
/*
省略最后再做一些预处理,比如通过width和len,计算出要补多少位,
用空格还是0等等
*/
// 直接拷贝pbuf内容到预处理好的res中
Py_MEMCPY(res, pbuf, len);
}
return result;
}
接下来看看formatint到底干了啥。。
具体我也没细看,不过从结构上可以看到,先用输入的prec,type和flags先格式化生成一次格式字符串。得到新的fmt。然后再以这个fmt去格式化PyObject v中的内容。无论是第一步“格式化生成格式字符串”,还是第二步“格式化生成最终字符串”,都是用C的sprintf去得到的。
Py_LOCAL_INLINE(int)
formatint(char *buf, size_t buflen, int flags,
int prec, int type, PyObject *v)
{
/* fmt = '%#.' + `prec` + 'l' + `type`
worst case length = 3 + 19 (worst len of INT_MAX on 64-bit machine)
+ 1 + 1 = 24 */
char fmt[64]; /* plenty big enough! */
char *sign;
long x;
x = PyInt_AsLong(v);
if (x == -1 && PyErr_Occurred()) {
PyErr_Format(PyExc_TypeError, "int argument required, not %.200s",
Py_TYPE(v)->tp_name);
return -1;
}
if (x < 0 && type == 'u') {
type = 'd';
}
if (x < 0 && (type == 'x' || type == 'X' || type == 'o'))
sign = "-";
else
sign = "";
if (prec < 0)
prec = 1;
if ((flags & F_ALT) &&
(type == 'x' || type == 'X')) {
/* When converting under %#x or %#X, there are a number
* of issues that cause pain:
* - when 0 is being converted, the C standard leaves off
* the '0x' or '0X', which is inconsistent with other
* %#x/%#X conversions and inconsistent with Python's
* hex() function
* - there are platforms that violate the standard and
* convert 0 with the '0x' or '0X'
* (Metrowerks, Compaq Tru64)
* - there are platforms that give '0x' when converting
* under %#X, but convert 0 in accordance with the
* standard (OS/2 EMX)
*
* We can achieve the desired consistency by inserting our
* own '0x' or '0X' prefix, and substituting %x/%X in place
* of %#x/%#X.
*
* Note that this is the same approach as used in
* formatint() in unicodeobject.c
*/
PyOS_snprintf(fmt, sizeof(fmt), "%s0%c%%.%dl%c",
sign, type, prec, type);
}
else {
PyOS_snprintf(fmt, sizeof(fmt), "%s%%%s.%dl%c",
sign, (flags&F_ALT) ? "#" : "",
prec, type);
}
/* buf = '+'/'-'/'' + '0'/'0x'/'' + '[0-9]'*max(prec, len(x in octal))
* worst case buf = '-0x' + [0-9]*prec, where prec >= 11
*/
if (buflen <= 14 || buflen <= (size_t)3 + (size_t)prec) {
PyErr_SetString(PyExc_OverflowError,
"formatted integer is too long (precision too large?)");
return -1;
}
if (sign[0])
PyOS_snprintf(buf, buflen, fmt, -x);
else
PyOS_snprintf(buf, buflen, fmt, x);
return (int)strlen(buf);
}
通过对比,可清晰地看到。int的tp_str的基础功能,我们自己都能写得出来。而连续两次sprintf格式化操作,可复杂多了。这里通过放出PyInt_Type的tp_str指针所指向的int_to_decimal_string函数来感受一下对比。
static PyObject *
int_to_decimal_string(PyIntObject *v) {
char buf[sizeof(long)*CHAR_BIT/3+6], *p, *bufend;
long n = v->ob_ival;
unsigned long absn;
p = bufend = buf + sizeof(buf);
absn = n < 0 ? 0UL - n : n;
do {
*--p = '0' + (char)(absn % 10);
absn /= 10;
} while (absn);
if (n < 0)
*--p = '-';
return PyString_FromStringAndSize(p, bufend - p);
}
综上所述
%d比%s复杂的原因就是%d它不是我们所想象的那么简单。Python作者得考虑一连串复杂的格式化选项。只是这些参数我们平常用不到而已。
在Python编码中,建议就是,如果我们只是单纯的%d而不带任何选项,那么使用%s会好得多。
除非修改源码,当没有指定prec,flags的时候,直接走tp_str。