前几日使用strip时遇到一个问题,在文档中找到了答案,好奇strip具体是如何实现的,就跟踪了下源码。那么一起看看吧。
问题描述
1 | 'abc_00005.md5'.strip('.md5') |
本意是想去掉.md5
,保留abc_00005
,但实际结果却是abc_0000
。
stackoverflow上也有类似的提问:
https://stackoverflow.com/questions/7853914/strange-behaviour-of-python-strip-function
在官方文档中找到了答案:
https://docs.python.org/zh-cn/3/library/stdtypes.html#str.strip
根本原因是没有深刻理解strip
的功能,导致误用。
源码分析
Python3中默认对字符串采用的是Unicode编码的str类型来表示,在Python文档中有描述:
Textual data in Python is handled with str objects, or strings. Strings are immutable sequences of Unicode code points.
上一篇文章在Windows上编译CPython中有提到源码中Objects目录为内置数据类型实现,其中Objects/unicodeobject.c
便是str数据类型相关的实现了。Unicode字符串的创建方式有多种,详见官方文档: Creating and accessing Unicode strings
我们从PyUnicode_New
函数开始看起
1 | PyObject *PyUnicode_New(Py_ssize_t size, Py_UCS4 maxchar) |
重点在PyObject_INIT
这一句中的PyUnicode_Type
,文档中有描述:
This instance of PyTypeObject represents the Python Unicode type. It is exposed to Python code as str.
1 | PyTypeObject PyUnicode_Type = { |
其中的unicode_methods
包含了str类型的数据的操作方法:
1 | static PyMethodDef unicode_methods[] = { |
我这里关注的是strip方法,在Objects/clinic/unicodeobject.c.h
中可以看到宏定义:
1 |
|
这里插播一句,在UNICODE_STRIP_METHODDEF
上面可以看到关于strip的文档描述:
1 | PyDoc_STRVAR(unicode_strip__doc__, |
其对应在解释器中通过help函数查看的结果:
1 | Python 3.7.7 (default, Jun 21 2020, 15:02:27) [MSC v.1916 64 bit (AMD64)] on win32 |
言归正传,接下来看UNICODE_STRIP_METHODDEF
宏定义中的unicode_strip
函数:
1 | static PyObject * |
主要调用了unicode_strip_impl
函数
1 | /*[clinic input] |
BOTHSTRIP
的宏定义如下:
1 |
do_argstrip
会根据参数的情况进入不同的处理流程:
1 | static PyObject * |
因为我出现的问题属于有参数的情况,会进入到_PyUnicode_XStrip
中:
1 | /* externally visible for str.strip(unicode) */ |
其中的kind
是表示unicode对象里面真正的字节的存储方式:
1 | enum PyUnicode_Kind { |
len
是用来获取原始Unicode字符串的长度,seplen
是作为参数的字符串长度:
1 | /* Returns the length of the unicode string. The caller has to make sure that |
sepmask
是十分重要的一步,后面的计算均由此有关,但我暂时还没完全看懂其中的含义,只列出make_bloom_mask
函数中的注释好了:
calculate simple bloom-style bitmask for a given unicode string
接下来会根据striptype
和sepmask
的值决定如何处理,但都会进入到PyUnicode_FindChar
中。
顺便看看do_strip
函数,其适用于strip
不存在任何参数的情况,do_strip
与_PyUnicode_XStrip
的处理逻辑大体相同,但会判断字符串是否只包含ASCII字符,这可以加快处理速度,如果是,其中会通过_Py_ascii_whitespace
数组索引来检查一个字符是否为空白字符
1 | static PyObject * |
PyUnicode_FindChar
使用给定的方向返回字符ch在str[start:end]
中的第一个位置(方向==1表示向前搜索,方向==-1表示向后搜索),但随着字符串长度的增加,通过连续调用此函数引入的开销也会增加:
1 | Py_ssize_t |
在此函数中,会经由findchar
进入到ucs1lib_find_char
中,先看一下STRINGLIB(find_char)
的写法:
1 |
利用##
连接符在宏定义中,将字符串ucs1lib_
和经参数F
传递过来的字符串连接起来,即ucs1lib_find_char
:
1 |
|
其中的s为源字符串,n为查找长度,ch为查找字符,里面用了一系列的检查方式来提高搜索的效率。_PyUnicode_XStrip
处理完毕后进入到PyUnicode_Substring
处理流程中,返回str的子串,从字符索引开始(包含)到字符索引结束(排除),其中判断是否只包含ASCII字符,如果不是,PyUnicode_FromKindAndData
会根据PyUnicode_KIND
返回的值来创建新的Unicode对象:
1 | PyObject* |
我这里会进入到_PyUnicode_FromASCII
函数,通过memcpy
函数进行收尾工作:
1 | PyObject* |
再看看PyUnicode_FromKindAndData
好了,就很简单的逻辑,直接用PyUnicode_KIND
返回值做switch case
:
1 | PyObject* |
以_PyUnicode_FromUCS1
为例可以看出处理逻辑与_PyUnicode_FromASCII
函数大体一致:
1 | static PyObject* |
结束
到这里分析完strip
相关的源码,处理流程就十分清晰了,算是解除了困惑,以后有空还是要多看看Python源码才行😀
参考
1、https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str
2、https://docs.python.org/zh-cn/3/c-api/unicode.html
3、https://docs.python.org/3/c-api/structures.html#c.PyMethodDef
4、https://www.python.org/dev/peps/pep-0393/
5、https://stackoverflow.com/a/38285655