静态整型 Cython 的变量和 Python 的变量是等价的,只不过前者是静态的,后者是动态的,而动态变量可以使用的 API,静态变量都可以使用。只不过对于 int 和 float 来说,C 里面也存在同名的类型,而且会优先使用 C 的类型,这也是我们期望的结果。 而一旦使用的是 C 里面的 int 和 float,比如 cdef int a = 1, cdef float b = 22.33,那么 a 和 b 就不再是指针了,它们代表的就是 C 的整数和浮点数。 那为什么在使用 int 和 float 的时候,要选择 C 的 int 和 float 呢?答案很好理解,因为 Cython 本身就是用来加速计算的,而提到计算,显然避不开 int 和 float,因此这两位老铁默认使用 C 里面的类型。 事实上单就 Python 的整数和浮点数来说,在运算时底层也是先转化成 C 的类型,然后再操作,最后将操作完的结果再转回 Python 的类型。而如果默认使用 C 的类型,就少了转换这一步,可以极大地提高效率。 但我们要知道 C 的整型是有范围的,我们在使用的时候要确保数值的大小不会溢出,这一点前面已经说过了。但是除此之外,还有一个重要的区别,就是除法和取模,在除法和取模上,C 的整型使用的却不是 C 的标准。 当使用有符号整数计算模的时候,C 和 Python 有着明显不同的行为:比如 -7 % 5,如果是 Python 的话那么结果为 3,C 的话结果为 -2。显然 C 的结果是符合我们正常人思维的,但是为什么 Python 得到的结果这么怪异呢? 事实上不光是 C,Go、Js 也是如此,计算 -7 % 5 的结果都是 -2,但 Python 得到 3,原因就是因为其内部的机制不同。我们知道 a % b,等于 a - (a / b) * b,其中 a / b 表示两者的商。比如 7 % 2,等于 7 - (7 / 2) * 2 = 7 - 3 * 2 = 1,对于正数,显然以上所有语言计算的结果都是一样的。 而负数出现差异的原因就在于:C 在计算 a / b 的时候是截断小数点,而 Python 是向下取整。比如上面的 -7 % 5,等于 -7 - (-7 / 5) * 5。-7 / 5 得到的结果是负的一点多,C的话直接截断得到 -1,因此结果是 -7 - (-1) * 5 = -2;但 Python 是向下取整,负的一点多变成 -2,因此结果变成了 -7 - (-2) * 5 = 3。
因此在除法和取模方面,尤其需要注意。另外即使在 Cython 中,也是一样的。
以上打印的结果都是 -2,说明 Cython 默认使用 Python 的语义执行除法操作,当然还有取模,即使操作的对象是静态类型的 C 标量。这么做原因就在于为了最大程度的和 Python 保持一致,如果想要启动 C 语义都需要显式地进行开启。 然后我们看到 a 和 b 是静态类型的 C 变量,它们也是可以使用 // 的,因为 Cython 的目的就像写 Python 一样。但我们看到无论是 c1 还是 c2,打印的结果都是 -2,这很好理解。 首先 c1 和 c2 都是静态的 int,在赋值的时候会将浮点数变成整数,至于是直接截断还是向下取整则是和 Python 保持一致的,是按照 Python 的标准来的。 a / b 得到的是 -1.4,在赋值给 int 类型的 c1 时会向下取整。至于 a // b 就更不用说了,a // b 本身就表示整除,因此 c2 也是 -2。然后我们再来举个浮点数的例子。
a / b 是 -1.4,但此时的 c1 是浮点数,所以没有必要取整了,小数位会保留;而 a // b虽然得到的也是浮点(只要 a 和 b 中有一个是浮点,那么 a / b 和 a // b 得到的也是浮点),但它依旧具备整除的意义,所以 a // b 得到结果是 -2.0,然后赋值给一个 float 变量,还是 -2.0。 关于 Python 中 / 和 // 在不同操作数之间的差异,我们再举个栗子看一下:
所以 Python 的整除或者说地板除还是比较奇葩的,主要原因就在于其它语言是截断(小数点后面直接不要了),而 Python 是向下取整。 如果是结果为正数的话,截断和向下取整是等价的,所以此时基本所有语言都是一样的。 而结果为负数的话,那么截断和向下取整就不同了,因为 -3.14 截断得到的是 -3、但向下取整得到的不是 -3,而是 -4。因此这一点务必要记住,算是 Python 的一个坑吧。话说如果没记错的话,好像只有 Python 采用了向下取整这种方式,别的语言(至少C、JS、Go)都是截断的方式。 还有一个问题,那就是整数和浮点数之间可不可以相互赋值呢?先说结论:
前面说了,使用 cdef int、cdef float 声明的变量不再是指向 Python 整数对象、浮点数对象的指针,而是 C 在栈上分配的整数和浮点数。尽管 C 整数没有考虑溢出,但是它在做运算的时候是遵循 Python 的规则(主要是除法),那么可不可以让其强制遵循 C 的规则呢?
文件名还是叫 cython_test.pyx,我们来测试一下。
除了装饰器的方式,还可以用下面两种方式来指定。 1)通过上下文管理器的方式
2)通过注释的方式进行全局声明
通过这三种方式,在 Cython 中可以让 C 整型变量的除法遵循 C 的语义。 这里再选择不使用 cython.cdivision,执行一下看看。
a 和 b 都是 C 的 int,相除得到的默认还是 int,而我们没有使用 cython.cdivision,那么默认使用 Python 的语义。相除之后的 -3.5 会向下取整,所以结果不是 -3,而是 -4。 总结:
另外 Cython 中还有一个 cdivision_warnings,使用方式和 cdivision 完全一样,表示:当取模的时候如果两个操作数中有一个是负数,那么会抛出警告。
测试一下:
另外这里的警告同时针对 Python 和 C,即使我们事先使用 @cython.cdivision(True) 装饰、将其改变为 C 的语义,也一样会弹出警告的。个人觉得 cdivision_warnings 意义不是很大,了解一下即可。 引用计数和静态字符串类型 我们知道解释器会自动管理内存,方法是通过引用计数来判断一个对象是否应该被回收,引入计数为 0 则对象回收,否则不回收。但是引用计数无法解决循环引用,于是又引入了垃圾回收来弥补引用计数的缺陷。 而 Cython 也会为我们处理所有的引用计数问题,确保 Python 对象(无论是 Cython 静态声明、还是 Python 动态声明)在引用计数为 0 时被销毁。 很好理解,就是说内存管理的问题 Cython 也会负责的。其实不用想也大概能猜到 Cython 会这么做,毕竟 cdef tuple a = (1, 2, 3) 和 a = (1, 2, 3) 底层都指向 PyTupleObject,只不过后者在操作的时候需要先通过 PyObject * 获取类型然后再转化,而前者则省略了这一步。 但它们底层都是 CPython 中的结构体,所以内存都由解释器管理。还是那句话,Cython 代码是要被翻译成 C 代码的,在翻译的时候会自动处理内存的问题,当然这点和 Python 也是一样的。 但是当 Cython 中动态变量和静态变量混合时,那么内存管理会有微妙的影响。我们举个栗子:
编译的时候会失败,咦,不是说后面可以跟一个 bytes 对象吗?但问题是这个 bytes 对象是一个临时对象,什么是临时对象呢?就是创建完了但是没有变量指向它,准确的说是没有 Python 类型的变量指向它。 因为这里的 name 使用的是 C 的类型,所以它不会增加这个 bytes 对象的引用计数。因此这个 bytes 对象创建出来之后就会被销毁。编译时会抛出:Storing unsafe C derivative of temporary Python reference,告诉我们创建出来的Python对象是临时的。 那么如何解决这一点呢?答案是使用变量保存起来就可以了。
所以 char * 比较特殊,它底层是使用一个指针来表示字符串。和整型和浮点型不同,cdef long a = 123,这个 123 直接就是 C 中的 long,可以直接使用。 但将 Python 的 bytes 对象赋值给 char *,在 C 的级别 char * 所引用的数据还是由 CPython 进行管理的,因为 bytes 对象内部有一个缓冲区,负责存储具体的数据,而 char * 会直接指向这个缓冲区。但它无法告诉解释器还有一个变量(非 Python 类型的变量)引用它,这就导致了 bytes 对象的引用计数不会加1,而是创建完之后就会被销毁。而 bytes 对象都销毁了,char * 类型的变量也就拿不到内部的数据了。 所以我们需要提前使用 Python 类型的变量(不管是静态声明还是动态声明)将其保存起来,让其引用计数加 1,这样就不会删除了。 那么下面的代码有没有问题呢?如果有问题该怎么改呢?
会不会出问题呢?显然会有大问题,尽管 word1 和 word2 指向了相应的 bytes 对象,但是 word1 + word2 则是会创建一个新的 bytes 对象,这个新的 bytes 对象可没有变量指向。所以这个新创建的 bytes 对象注定是昙花一现,创建完之后会被立刻销毁,因此无法赋值给 char * 变量。 另外创建 char * 还有一种方式:
此时的 "satori" 会被当成是 C 的字符串,所以这种做法也是可以的,不过很明显,它只能是 ascii 字符串。 但下面这种做法不行:
char * 需要接收 C 的字符串,但我们赋值给了一个变量,那么它就是 Python 类型了,而 Python 的 str 和 C 的 char * 无法直接转化,两者没有对应关系。于是报错:TypeError: expected bytes, str found。 而 char * 和 Python 的 bytes 是对应的,每个元素都是 0 到 255 的整数。
同理 char * 在赋值给 Python 类型的变量时,也会自动转成 bytes 对象,因为这两者是对应的。
以上就是 char * 相关的内容,它表示 C 的字符串类型,对应 Python 的 bytes。显然它在操作的时候,速度要比 bytes 对象快很多,如果你希望程序运行的更快一些,那么不妨将 bytes 类型替换成 char * 类型。 因此关于 char * 来总结一下: 当然啦,char * 是 C 的字符串类型,Python 也有自己的字符串类型,也就是 str。
比较简单,没什么可说的。然后 Cython 还提供了一个 Py_UCS4,它表示只有一个字符的字符串。
以上代码会编译失败,另外 Py_UCS4 表示一个 UNICODE,所以这个 Py_UCS4 还可以换成 Py_UNICODE,效果是一样的,都表示长度为 1 的 Python 字符串。 以上就是字符串相关的内容,str 表示 Python 的字符串类型,char * 表示 C 的字符串类型,对应 Python 的 bytes 类型。使用 char * 的速度会更快,asyncpg 这个数据库驱动在解析数据时就将 bytes 换成了 char *,速度从而得到了很大的提升。 但我们说,C 的类型虽然速度快,可是不够灵活,它使用起来肯定没有 bytes 对象方便。如果你的程序没有到达性能瓶颈,可以考虑不使用 char *,直接使用 bytes 和 str 就行。通过 cdef bytes 和 cdef str 静态声明,速度依旧可以提升,至于要不要使用 char * 就看你自己的习惯了。 小结 以上就是静态整形和静态字符串类型相关的内容,在使用的时候会有一些意想不到的小陷阱,所以需要注意。 下一篇文章我们来说一说 Cython 中的函数。 |
|