某一天吹水的时候,吹着吹着就吹到了一下这么一个案例。
import timeitdef 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_MODULO7 POP_TOP8 LOAD_CONST 0 (None)11 RETURN_VALUE
None
============ b ============8 0 LOAD_CONST 1 ('%s, %d')3 LOAD_CONST 4 ((1, 2))6 BINARY_MODULO7 POP_TOP8 LOAD_CONST 0 (None)11 RETURN_VALUE
None
============ c ============11 0 LOAD_CONST 1 ('%d, %d')3 LOAD_CONST 4 ((1, 2))6 BINARY_MODULO7 POP_TOP8 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应该差不多。
那么都有哪些操作呢?
可以这么理解,上述的功能描述里,带“数字”字眼的,都只对整数生效。我们来举个例子。
>>> "%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 = "-";elsesign = "";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);elsePyOS_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。