as ctrl s格式化代码_玩转Python源码(一) "%s"与“%d”

bb36e2db837e67c7f8979aeb9ab23a88.png

某一天吹水的时候,吹着吹着就吹到了一下这么一个案例。

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应该差不多。

那么都有哪些操作呢?

c7f05931dfd92a166117514dc4682beb.png
截图自菜鸟教程(https://www.runoob.com/python/python-strings.html)

可以这么理解,上述的功能描述里,带“数字”字眼的,都只对整数生效。我们来举个例子。

>>> "%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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值