Cython处理C字符串的示例详解
目录
- 楔子
- 创建 C 字符串
- 引用计数陷阱
- strlen
- strcpy
- strcat
- strcmp
- sprintf
- 动态申请字符串内存
- memset
- memcpy
- memmove
- memcmp
- 小结
楔子
在介绍数据类型的时候我们说过,python 的数据类型相比 C 来说要更加的通用,但速度却远不及 C。如果你在使用 Cython 加速 Python 时遇到了瓶颈,但还希望更进一步,那么可以考虑将数据的类型替换成 C 的类型,特别是那些频繁出现的数据,比如整数、浮点数、字符串。
由于整数和浮点数默认使用的就是 C 的类型,于是我们可以从字符串入手。
创建 C 字符串
先来回顾一下如何在 Cython 中创建 C 字符串。
cdefchar*s1=b"abc" print(s1)#b'abc'
C 的数据和 Python 数据如果想互相转化,那么两者应该存在一个对应关系,像整数和浮点数就不必说了。但 C 的字符串本质上是一个字符数组,所以它和 Python 的 bytes 对编程客栈象是对应的,我们可以将 b"abc" 直接赋值给 s1。并且在打印的时候,也会转成 Python 的 bytes 对象之后再打印。
或者还可以这么做:
cdefchars1[4] s1[0],s1[1],s1[2]=97,98,99 cdefbytess2=bytes([97,98,99]) print(s1)#b'abc' print(s2)#b'abc'
直接声明一个字符数组,然后再给数组的每个元素赋值即可。
Python 的 bytes 对象也是一个字符数组,和 C 一样,数组的每个元素不能超过 255,所以两者存在对应关系。在赋值的时候,会相互转化,其它类型也是同理,举个例子:
#Python整数和C整数是存在对应关系的 #因为都是整数,所以可以相互赋值 py_num=123 #会根据Python的整数创建出C的整数,然后赋值给c_num cdefunsignedintc_num=py_num #print是Python的函数,它接收的一定是PyObject* #所以在打印C的整数时,会转成Python的整数再进行打印 print(c_num,py_num) """ 123123 """ #但如果写成cdefunsignedintc_num="你好"就不行了 #因为Python的字符串和C的整数不存在对应关系 #两者无法相互转化,自然也就无法赋值 #浮点数也是同理,Python和C的浮点数可以相互转化 cdefdoublec_pi=3.14 #赋值给Python变量时,也会转成Python的浮点数再赋值 py_pi=3.14 print(c_pi,py_pi) """ 3.143.14 """ #Python的bytes对象和C的字符串可以相互转化 cdefbytespy_name=bytes("古明地觉",encoding="utf-8") cdefchar*c_name=py_name print(py_name==c_name) """ True """ #注意:如果 Python 字符串所有字符的 ASCII 均不超过 255 #那么也可以赋值给C字符串 cdefchar*name1="satori" cdefchar*name2=b"satori" print(name1,name2) """ b'satori'b'satori' """ #"satori"会直接当成C字符串来处理,因为它里面的字符均为ASCII #就像写C代码一样,所以name1和name2是等价的 #而在转成Python对象的时候,一律自动转成bytes对象 #但是注意:cdef char *c_name ="古明地觉"这行代码不合法 #因为里面出现了非ASCII字符,所以建议在给C字符串赋值的时候,一律使用bytes对象 #C的结构体和Python的字典存在对应关系 ctypedefstructGirl: char*name intage cdefGirlg g.name,g.age=b"satori",17 #在打印的时候,会转成字典进行打印 #当然前提是结构体的所有成员,都能用Python表示 print(g) """ {'name':b'satori','age':17} """
所以 Python 数据和 C 数据是可以互相转化的,哪怕是结构体,也是可以的,只要两者存在对应关系,可以互相表示。但像指针就不行了,Python 没有任何一种原生类型能和 C 的指针相对应,所以 print 一个指针的时候就会出现编译错误。
以上这些都是之前介绍过的内容,但很多人可能都忘了,这里专门再回顾一下。
引用计数陷阱
这里需要再补充一个关键点,由于 bytes 对象实现了缓冲区协议,所以它内部有一个缓冲区,这个缓冲区内部存储了所有的字符。而在基于 bytes 对象创建 C 字符串的时候,不会拷贝缓冲区里的内容(整数、浮点数都是直接拷贝一份),而是直接创建一个指针指向这个缓冲区。
#合法的代码 py_name="古明地觉".encode("utf-8") cdefchar*c_name1=py_name #不合法的代码,会出现如下编译错误 #StoringunsafeCderivativeoftemporaryPythonreference cdefchar*c_name2="古明地觉".encode("utf-8")
为啥在创建 c_name2 的时候就会报错呢?很简单,因为这个过程中进行了函数调用,所以产生了临时对象。换句话创建的 bytes 对象是临时的,这行代码执行结束后就会因为引用计数为 0 而被销毁。
问题来了,c_name2 不是已经指向它了吗?引用计数应该为 1 才对啊。相信你能猜到原因,这个 c_name2 的类型是 char *,它是一个 C 的变量,不会增加对象的引用计数。这个过程就是创建了一个 C 级指针,指向了临时的 bytes 对象内部的缓冲区,而解释器是不知道的。
所以临时对象最终会因为引用计数为 0 被销毁,但是这个 C 指针却仍指向它的缓冲区,于是就报错了。我们需要先创建一个 Python 变量指向它,让其不被销毁,然后才能赋值给 C 级指针。为了更好地说明这个现象,我们使用 bytearray 举例说明。
cdefbytearraybuf=bytearray("hello",encoding="utf-8") cdefchar*c_str=buf print(buf)#bytearray(b'hello') #基于c_str修改数据 c_str[0]=ord("H") #再次打印buf print(buf)#bytearray(b'Hello') #我们看到buf被修改了
bytearray 对象可以看作是可变的 bytes 对象,它们内部都实现了缓冲区,但 bytearray 对象的缓冲区是可以修改的,而 bytes 对象的缓冲区不能修改。所以这个例子就证明了上面的结论,C 字符串会直接共享 Python 对象的缓冲区。
因此在赋值的时候,我们应该像下面这么做。
print( "你好".encode("utf-8") )#b'\xe4\xbd\xa0\xe5\xa5\xbd' #如果出现了函数或类的调用,那么会产生临时对象 #而临时对象不能直接赋值给C指针,必须先用Python变量保存起来 cdefbytesgreet="你好".encode("utf-8") cdefchar*c_greet1=greet #如果非要直接赋值,那么赋的值一定是字面量的形式 #这种方式也是可以的,但显然程序开发中我们不会这么做 #除非它是纯ASCII字符 #比如cdefchar*c_greet2=b"hello" cdefchar*c_greet2=b"\xe4\xbd\xa0\xe5\xa5\xbd" print(c_greet1.decode("utf-8"))#你好 print(c_greet2.decode("utf-8"))#你好
以上就是 C 字符串本身相关的一些内容。
那么重点来了,假设我们将 Python 的字符串编码成 bytes 对象之后,赋值给了 C 字符串,那么 C 语言都提供了哪些 API 让我们去操作呢?
strlen
strlen 函数会返回字符串的长度,不包括末尾的空字符。C 字符串的结尾会有一个 \0,用于标识字符串的结束,而 strlen 不会统计 \0。
#C的库函数,一律通过libc进行导入 fromlibc.stringcimportstrlen cdefchar*s=b"satori" print(strlen(s))#6
注意:strlen 和 sizeof 是两个不同的概念,strlen 计算的是字符串的长度,只能接收字符串。而 sizeof 计算的是数据所占用的内存大小,可以接收所有 C 类型的数据。
fromlibc.stringcimportstrlen cdefchars[50] #strlen是从头遍历,只要字符不是\0,那么数量加1 #遇到\0停止遍历,所以strlen计算的结果是0 print(strlen(s))#0 #而sizeof计算的是内存大小,当前数组s的长度为50 print(sizeof(s))#50 s[0]=97 print(strlen(s))#1 s[1]=98 print(strlen(s))#2 print(sizeof(s))#50
当然啦,你也可以手动模拟 strlen 函数。
fromlibc.stringcimportstrlen cdefssize_tmy_strlen(constchar*string): """ 计算C字符串string的长度 """ cdefssize_tcount=0 whilestring[count]!=b"\0": count+=1 returncount cdefchar*name=b"HelloCruelWorld" print(strlen(name))#17 print(my_strlen(name))#17
还是很简单的,当然啦,我们也可以调用内置函数 len 进行计算,结果也是一样的。只不过调用 len 的时候,会先基于 C 字符串创建 bytes 对象,这会多一层转换,从而影响效率。
strcpy
然后是拷贝字符串,这里面有一些需要注意的地方。
fromlibc.stringcimportstrcpy cdefcharname[10] strcpy(name,b"satori") print(name)#b'satori' strcpy(name,b"koishi") print(name)#b'koishi' #以上就完成了字符串的拷贝,但要注意name是数组的名字 #我们不能给数组名赋值,比如name=b"satori" #这是不合法的,因为它是一个常量 #我们需要通过name[索引]或者strcpy的方式进行修改 #或者还可以这么做,创建一个bytearray对象,长度10 #注意:这里不能用 bytes 对象,因为 bytes 对象的缓冲区不允许修改 cdefbuf=bytearray(10) cdefchar*name2=buf strcpy(name2,b"marisa") print(buf)#bytearray(b'marisa\x00\x00\x00\x00') print(name2)#b'marisa' #不过还是不建议使用bytearray作为缓冲区 #直接通过cdefcharname2[10]声明即可
char name[10] 这种形式创建的数组是申请在栈区的,如果想跨函数调用,那么应该使用 malloc 申请在堆区。
然后 strcpy 这个函数存在一些隐患,就是它不会检测目标字符串是否有足够的空间去容纳源字符串,因此可能导致溢出。
fromlibc.stringcimportstrcpy cdefcharname[6] #会发生段错误,解释器异常退出 #因为源字符串有6个字符,再加上一个\0 #那么name的长度至少为7才可以 strcpy(name,b"satori") print(name)
因此如果你无法保证一定不会发生溢出,那么可以考虑使用 strncpy 函数。它和 strcpy 的用法完全一样,只是多了第三个参数,用于指定复制的最大字符数,从而防止目标字符串发生溢出。
第三个参数 size 定义了复制的最大字符数,如果达到最大字符数以后,源字符串仍然没有复制完,就会停止复制。如果源字符串的字符数小于目标字符串的容量,则 strncpy 的行为与 strcpy 完全一致。
fromlibc.stringcimportstrncpy cdefcharname[6] #最多拷贝5个字符,因为要留一个给\0 strncpy(name,b"satori",5) print(name)#b'sator' #当然,即使目标字符串容量很大,我们也可以只拷贝一部分 cdefcharwords[100] strncpy(words,b"helloworld",5) print(words)#b'hello'
以上就是字符串的拷贝,并且对于目标字符串来说,每一次拷贝都相当于一次覆盖,什么意思呢?举个例子。
fromlibc.stringcimportstrcpy cdefcharwords[10] strcpy(words,b"abcdef") #此时的words就是{a,b,c,d,e,f,\0,\0,\0,\0} #然后我们继续拷贝,会从头开始覆盖 strcpy(words,b"xyzwww.devze.com") #此时的words就是{x,y,z,\0,e,f,\0,\0,\0,\0} #因为字符串自带\0,所以z的结尾会有一个\0 #而C字符串在遇到\0的时候会自动停止 print(words)#b'xyz' #将words[3]改成d words[3]=ord("d") print(words)#b'xyzdef'
所以要注意 \0,它是 C 编译器判断字符串是否结束的标识。
strcat
strcat 函数用于连接字符串,它接收两个字符串作为参数,把第二个字符串的副本添加到第一个字符串的末尾。这个函数会改变第一个字符串,但是第二个字符串不变。
fromlibc.stringcimportstrcpy,strcat cdefcharwords1[20] strcpy(words1,b"Hello") print(words1)#b'Hello' strcpy(words1,b"World") print(words1)#b'World' cdefcharwords2[20] strcat(words2,b"Hello") print(words2)#b'Hello' strcat(words2,b"World") print(words2)#b'HelloWorld'
注意,strcat 会从目标字符串的第一个 \0 处开始,追加源字符串,所以目标字符串的剩余容量,必须足以容纳源字符串。否则拼接后的字符串会溢出第一个字符串的边界,写入相邻的内存单元,这是很危险的,建议使用下面的 strncat 代替。
strncat 和 strcat 的用法一致,但是多了第三个参数,用于指定追加的最大字符数。
fromlibc.stringcimportstrncat,strlen cdefchartarget[10] cdefchar*source=b"HelloWorld" #追加的最大字符数等于:容量-当前的长度- 1 strncat(target,source, sizeof(target)-strlen(target)-1) print(target)#b'HelloWor'
为了安全,建议使用 strncat。
strcmp
strcmp 用于字符串的比较,它会按照字符串的字典序比较两个字符串的内容。
fromlibc.stringcimportstrcmp #s1==s2,返回0 print( strcmp(b"abc",b"abc") )#0 #s1>s2,返回1 print( strcmp(b"abd",b"abc") )#1 #s1<s2,返回0 print( strcmp(b"abc",b"abd") )#-1
由于 strcmp 比较的是整个字符串,于是 C 语言又提供了 strncmp 函数。strncmp 增加了第三个参数,表示比较的字符个数。
fromlibc.stringcimportstrcmp,strncmp print( strcmp(b"abcdef",b"abcDEF") )#1 #只比较3个字符 print( strncmp(b"abcdef",b"abcDEF",3) )#0
比较简单,并且比较规则和 strcmp 一样。
sprintf
sprintf 函数 printf 类似,但是用于将数据写入字符串,而不是输出到显示器。
fromlibc.stdiocimportsprintf cdefchars1[25] sprintf(s1,b"name:%s,age:%d",b"satori",17) print(s1) """ b'name:satori,age:17' """ #也可以指向bytearray的缓冲区 cdefbuf=bytearray(25) cdefchar*s2=buf sprintf(s2,b"name:%s,age:%d",b"satori",17) print(s2) print(buf) """ b'name:satori,age:17' bytearray(b'name:satori,age:17\x00\x00\x00\x00') """ #或者申请在堆区 fromlibc.stdlibcimportmalloc cdefchar*s3=<char*>malloc(25) sprintf(s3,b"name:%s,age:%d",b"satori",17) print(s3) """ b'name:satori,age:17' """
同样的,sprintf 也有严重的安全风险,如果写入的字符串过长,超过了目标字符串的长度,sprintf 依然会将其写入,导致发生溢出。为了控制写入的字符串的长度,C 语言又提供了另一个函数 snprintf。
snprintf 多了一个参数,用于控制写入字符的最大数量。
fromlibc.stdiocimportsnprintf cdefchars1[10] #写入的字符数量不能超过:最大容量-1 snprintf(s1,sizeof(s1)-1, b"name:%s,age:%d",b"satori",17) print(s1) """ b'name:sa' """
建议使用 snprintf,要更加的安全,如果是 sprintf,那么当溢出时会发生段错误,这是一个非常严重的错误。
动态申请字符串内存
我们还可以调用 malloc, calloc, realloc 函数为字符串动态申请内存,举个例子:
fromlibc.stdlibcimport( mallojavascriptc,calloc ) fromlibc.stringcimportstrcpy #这几个函数所做的事情都是在堆上申请一块编程客栈内存 #并且返回指向这块内存的void*指针 cdefvoid*p1=malloc(4) #我们想用它来存储字符串,那么就将void*转成char* strcpy(<char*>p1,b"abc") #或者也可以这么做 cdefchar*p2=<char*>malloc(4) strcpy(p2,b"def") print(<char*>p1)#b'abc' print(p2)#b'def' #当然,申请的内存不光可以存储字符串,其它数据也是可以的 cdefint*p3=<int*>malloc(8) p3[0],p3[1]=11,22 print(p3[0]+p3[1])#33 #以上是malloc的用法,然后是calloc #它接收两个参数,分别是申请的元素个数、每个元素占用的大小 cdefint*p4=<int*>calloc(10,sizeof(int)) #它和下面是等价的 cdefint*p5=<int*>calloc(10*4)
如果是在 C 里面,那么 malloc 申请的内存里面的数据是不确定的,而 calloc 申请的内存里面的数据会被自动初始化为 0。但在 Cython 里面,它们都会被初始化为 0。
并且还要注意两点:
- 1)malloc 和 calloc 在申请内存的时候可能会失败,如果失败则返回 NULL,因此在申请完之后最好判断一下指针是否为 NULL;
- 2)malloc 和 calloc 申请的内存都在堆区,不用了之后一定要调用 free 将内存释放掉,free 接收一个 void *,用于释放指向的堆内存。当然啦,为了安全起见,在释放之前,先判断指针是否为 NULL,不为 NULL 再释放;
最后一个函数是 realloc,它用于修改已经分配的内存块的大小,可以放大也可以缩小,返回一个指向新内存块的指针。
fromlibc.stdlibcimport( malloc,realloc ) fromlibc.stringcimportstrcpy cdefchar*p1=<char*>malloc(4) strcpy(p1,b"abc") #p1指向的内存最多能容纳3个有效字符串 #如果希望它能容纳更多,那么就要重新分配内存 p1=<char*>realloc(p1,8) #如果新内存块小于原来的大小,则丢弃超出的部分; #大于原来的大小,则返回一个全新的地址,数据也会自动复制过去 #如果第二个参数是0,那么会释放掉内存块 #如果realloc的第一个参数是NULL,那么等价于malloc cdefchar*p2=<char*>realloc(NULL,40) #等价于cdefchar*p2=<char*>malloc(40) #由于有分配失败的可能,所以调用realloc之后 #最好检查一下它的返回值是否为NULL #并且分配失败时,原有内存块中的数据不会发生改变。
在 C 里面,malloc 和 realloc 申请的内存不会自动初始化,一般申请完之后还要手动初始化为 0。但在 Cython 里面,一律会自动初始化为 0,这一点就很方便了。
memset
memset 是一个初始化函数,它的作用是将某一块内存的所有字节都设置为指定的值。
fromlibc.stdlibcimportmalloc fromlibc.stringcimportmemset #函数原型 #void*memset(void*block,intc,size_tsize) cdefchar*s1=<char*>malloc(10) memset(<void*>s1,ord('a'),10-1) #全部被设置成了a print(s1)#b'aaaaaaaaa' cdefchar*s2=<char*>malloc(10) #只设置前三个字节 memset(<void*>s2,ord('a'),3) print(s2)#b'aaa'
在使用 memset 的时候,一般都是将内存里的值都初始化为 0。
memcpy
memcpy 用于将一块内存拷贝到另一块内存,用法和 strncpy 类似,但前者不光可以拷贝字符串,任意内存都可以拷贝,所以它接收的指针是 void *。
fromlibc.stringcimportmemcpy cdefchartarget[10] cdefchar*source="HelloWorld" #接收的指针类型是void*,它与数据类型无关 #就是以字节为单位,将数据逐个拷贝过去 #并且还有第三个参数,表示拷贝的最大字节数 memcpy(<void*>target,<void*>source,9) print(target)#b'HelloWor' #同样的,整数数组也可以 cdefinttarget2[5] cdefintsource2[3] source2[0],source2[1],source2[2]=11,22,33 memcpy(<void*>target2,<void*>source2,5*sizeof(int)) print(target2[0],target2[1],target2[2])#11,22,33 #当然你也可以自己实现一个memcpy cdefvoidmy_memcpy(void*src,void*dst,ssize_tcount): #不管src和dst指向什么类型,统一当成1字节的char #逐个遍历,然后拷贝过去即可 cdefchar*s=<char*>src cdefchar*d=<char*>dst #在Cython里面解引用不可以通过*p的方式,而是要使用p[0] #因为*p这种形式在Python里面有另外的含义 whilecount!=0: s[0]=d[0] s+=1 d+=1 #测试一下 cdeffloattarget3[5] cdeffloatsource3[3] source3[0],source3[1],source3[2]=3.14,2.71,1.732 memcpy(<void*>target3,<void*>source3,5*sizeof(float)) print(target3[0],target3[1],target3[2])#3.14,2.71,1.732
所以在拷贝字符串的时候,memcpy 和 strcpy 都可以使用,但是推荐 memcpy,速度更快也更安全。
memmove
memmove 函数用于将一段内存数据复制到另一段内存,它跟 memcpy 的作用相似,用法也一模一样。但区别是 memmove 允许目标区域与源区域有重叠。如果发生重叠,源区域的内容会被更改;如果没有重叠,那么它与 memcpy 行为相同。
fromlibc.stringcimportmemcpy,memmove cdefchartarget1[20] cdefchartarget2[20] cdefchar*source="HelloWorld" #target1、target2和source均不重叠 #所以memcpy和memmove是等价的 memcpy(<void*>target1,<void*>source,20-1) memmove(<void*>target2,<void*>source,20-1) print(target1)#b'HelloWorld' print(target2)#b'HelloWorld' #但&target1[0]和&target[1]是有重叠的 #将target1[1:]拷贝到target1[0:],相当于每个字符往前移动一个位置 memmove(<void*>&target1[0],<void*>&target1[1],19-1) print(target1)#b'elloWorld' #显然此时内容发生了覆盖,这时候应该使用memmove
应该很好理解。
memcmp
memcmp 用于比较两个内存区域是否相同,前两个参数是 void * 指针,第三个参数比较的字节数,所以它的用法和 strncmp 是一致的。
fromlibc.stringcimportmemcmp,strncmp cdefchar*s1=b"Hello1" cdefchar*s2=b"Hello2" #s1==s2返回0;s1>=s2返回1;s1<=s2返回-1 print(memcmp(<void*>s1,<void*>s2,6))#-1 print(memcmp(<void*>s1,<void*>s2,5))#0 print(strncmp(s1,s2,6))#-1 print(strncmp(s1,s2,5))#0 #所以memcmp和strncmp的用js法是一样的 #但memcmp在比较的时候会忽略\0 cdefchars3[5] cdefchars4[5] #'\0'的ASCII码就是0 #所以s3就相当于{'a','b','c','\0','e'} s3[0],s3[1],s3[2],s3[3],s3[4]=97,98,99,0,100 #s4就相当于{'a','b','c','\0','f'} s4[0],s4[1],s4[2],s4[3],s4[4]=97,98,99,0,101 #strncmp在比较的时候,如果遇到\0,那么字符串就结束了 print(strncmp(s3,s4,5))#0 #memcmp支持所有数据类型的比较,不单单针对字符串 #所以它在比较的时候不会关注\0,就是逐一比较每个字节,直到达到指定的字节数 #因为e的ASCII码小于f,所以结果是-1 print(memcmp(<void*>s3,<void*>s4,5))#-1
以上就是 memcmp 的用法,我们总结一下出现的函数。
小结
以上就是在 Cython 中处理 C 字符串的一些操作,说实话大部分都是和 C 相关的内容,如果你熟悉 C 的话,那么这篇文章其实可以不用看。
因为 Cython 同理理解 C 和 Python,在加速的时候不妨把字符串换成 char * 试试吧。比如有一个操作 pgsql 的异步驱动 asyncpg 就是这么做的,因此速度非常快。
到此这篇关于Cython处理C字符串的示例详解的文章就介绍到这了,更多相关Cyth开发者_JAVA开发on处理C字符串内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!
精彩评论