关于python:通过-cdef-进行静态类型声明

2次阅读

共计 4417 个字符,预计需要花费 12 分钟才能阅读完成。

楔子
首先 Python 中申明变量的形式在 Cython 外面也是能够应用的,因为 Python 代码也是非法的 Cython 代码。

a = [x for x in range(12)]
b = a
a[3] = 42.0
assert b[3] == 42.0
a = "xxx"
assert isinstance(b, list)

在 Cython 中,没有类型化的动静变量的行为和 Python 完全相同,通过赋值语句 b = a 让 b 和 a 都指向同一个列表。在 a[3] = 42.0 之后,b[3] == 42.0 也是成立的,因而断言成立。

即使前面将 a 批改了,也只是让 a 指向了新的对象,调整相应的援用计数。而对 b 而言则没有受到丝毫影响,因而 b 指向的仍旧是一个列表。这是齐全非法、并且无效的 Python 代码。

而对于动态类型变量,咱们在 Cython 中通过 cdef 关键字进行申明,比方:

cdef int i
cdef int j
cdef float k
# 咱们看到就像应用 Python 和 C 的混合体一样
j = 0
i = j
k = 12.0
j = 2 * k
assert i != j

下面除了变量的申明之外,其它的应用形式和 Python 并无二致,当然简略的赋值的话,基本上所有语言都是相似的。然而 Python 的一些内置函数、类、关键字等等都是能够间接应用的,因为咱们在 Cython 中能够间接写 Python 代码,它是 Python 的超集。

然而有一点须要留神:咱们下面创立的变量 i、j、k 是 C 中的类型(int、float 比拟非凡,前面会解释),其意义最终要遵循 C 的规范。

不仅如此,就连应用 cdef 申明变量的形式也是依照 C 的规范来的。

cdef int i, j, k
cdef float x, y

# 申明的同时并赋值
cdef int a = 1, b = 2
cdef float c = 3.0, b = 4.1

而在函数外部,cdef 也是要进行缩进的,它们申明的变量也是一个局部变量。

def foo():
    # 这里的 cdef 是缩进在函数外部的
    cdef int i
    cdef int N = 2000
    # a 没有初始值,默认是零值,即 0.0
    cdef float a, b = 2.1

并且 cdef 还能够应用相似于 Python 上下文管理器的形式。

def foo():
    # 这种申明形式也是能够的
    # 和下面的形式是齐全等价的
    cdef:
        int i
        int N = 2000
        float a, b = 2.1
    # 然而申明变量时,要留神缩进
    # Python 对缩进是有考究的, 它规定了作用域
    # 所以 Cython 在语法方面还是保留了 Python 的格调

所以应用 cdef 申明变量非常简单,格局:cdef 类型 变量名。当然啦,同时也能够赋上初始值。

一旦应用 cdef 动态申明,那么后续再给变量赋值的时候,就不能那么得心应手了,举个例子:

# 如果是动静申明,以下都是非法的
# a 能够指向任意的对像,没有限度
a = 123
a = []

# 但如果是动态申明
# 那么 b 的类型必须是整型
cdef int b = 123
# 将一个列表赋值给 a 是会呈现编译谬误的
b = []  # compile error

也正是因为在编译阶段就能检测出类型,并调配好内存,所以在执行的时候速度才会快。

static 和 const

如果你理解 C 的话,那么思考一下:假如要在函数中返回一个局部变量的指针、并且内部在接管这个指针之后,还能拜访指针指向的值,这个时候该怎么办呢?咱们晓得 C 函数中的变量是调配在栈上的(不应用 malloc 函数,而是间接创立一个变量),函数完结之后变量对应的值就被销毁了,所以这个时候即便返回一个指针也是无意义的。

只管有些时候,在返回指针之后还是可能拜访指向的内存,但这只是以后应用的编译器比拟笨,在编译时没有检测进去。如果是高级一点的编译器,那么在拜访的时候会报出段谬误或者打印出一个谬误的值;而更高级的编译器甚至连指针都不让返回了,因为指针指向的内存曾经被回收了,那还要这个指针做什么?因而指针都不让返回了。

而如果想做到这一点,那么只须要在申明变量的同时在后面加上 static 关键字,比方 static int i,这样的话 i 这个变量就不会被调配到栈区,而是会被调配到数据区。数据区里变量的生命周期不会随着函数的完结而完结,而是随同着整个程序。

但惋惜的是,static 不是一个无效的 Cython 关键字,因而咱们无奈在 Cython 中申明一个 C 的 static 变量。

除了 static,在 C 中还有一个 const,用来申明常量。一旦应用 const 申明,比方 const int i = 3,那么这个 i 在后续就不能够被批改了。而在 Cython 中,const 是反对的,然而它只能在定义函数参数的时候应用,在介绍函数的时候再聊。

所以 C 的 static 和 const 目前在 Cython 中就无需太关注了。

C 类型

咱们下面申明变量的时候,指定的类型是 int 和 float,而在 Python 和 C 外面都有 int 和 float,那么用的到底是谁的呢?其实下面曾经说了,用的是 C 的 int 和 float,至于起因,咱们前面再聊。

而 Cython 能够应用的 C 类型不仅有 int 和 float,像 short, int, long, unsigned short, long long, size_t, ssize_t 等根底类型都是反对的,申明变量的形式均为 cdef 类型 变量名。申明的时候能够赋初始值,也能够不赋初始值。

而除了根底类型,还有指针、数组、定义类型别名、构造体、共同体、函数指针等等也是反对的,咱们前面细说。

Cython 的主动类型推断

Cython 还会对函数体中没有进行类型申明的变量主动执行类型推断,比方:for 循环外面全部都是浮点数相加,没有波及到其它类型的变量,那么 Cython 在主动对变量进行推断的时候会发现这个变量能够被优化为动态类型的 double。

但一个程序显然无奈对一个动静类型的语言进行十分智能的全方位优化,默认状况下,Cython 只有在确认这么做不会扭转代码块的语义之后才会进行类型推断。

看一个简略的函数:

def automatic_inference():
    i = 1
    d = 2.0
    c = 3 + 4j
    r = i * d + c
    return r

在这个例子中,Cython 会将赋给变量 i、c、r 的值标记为通用的 Python 对象。只管这些对象的类型和 C 的类型具备高度的相似性,但 Cython 会激进地推断 i 可能无奈用 C 的整型示意(C 的整数有范畴,而 Python 没有、能够无限大),因而会将其作为合乎 Python 代码语义的 Python 对象。

而对于 d = 2.0,则能够主动推断为 C 的 double,因为 Python 的浮点数对应的值在底层就是应用一个 double 来存储的。所以最终对于开发者来讲,变量 d 看似是一个 Python 的对象,然而 Cython 在执行的时候会将其视为 C 的 double 以进步性能。

这就是即便咱们写纯 Python 代码,Cython 编译器也能进行优化的起因,因为会进行推断。然而很显著,咱们不应该让 Cython 编译器去推断,而是明确指定变量的类型。

当然如果非要 Cython 编译器去猜,也是能够的,而且还能够通过 infer_types 编译器指令,在一些可能会扭转 Python 代码语义的状况下给 Cython 留有更多的余地来推断一个变量的类型。

cimport cython

@cython.infer_types(True)
def more_inference():
    i = 1
    d = 2.0
    c = 3 + 4j
    r = i * d + c
    return r

这里呈现了一个新的关键字 cimport,它的含意咱们当前会说,目前只须要晓得它和 import 关键字一样,是用来导入模块的即可。而后咱们通过装璜器 @cython.infer_types(True),启动了相应的类型推断,也就是给 Cython 留有更多的猜想空间。

当 Cython 反对更多推断的时候,变量 i 会被类型化为 C 的整型;d 和之前一样是 double,而 c 和 r 都是复数变量,复数则仍旧应用 Python 的复数类型。

然而留神:并不代表启用 infer_types 时,就高枕无忧了;咱们晓得在不指定 infer_types 的时候,Cython 推断类型显然是采纳最最保险的办法、在保障程序正确执行的状况下进行优化,不能为了优化而导致程序呈现谬误,显然正确性和效率之间,正确性是第一位的。

而 C 的整型因为存在溢出的问题,所以 Cython 不会擅自应用。然而咱们通过 infer_types 启动了更多的类型推断,让 Cython 在不扭转语义的状况下应用 C 的类型。然而溢出的问题它不晓得,所以在这种状况下是须要咱们来负责确保不会呈现溢出。

对于一个函数来说,如果启动这样的类型推断的话,咱们能够应用 infer_types 装璜器的形式。不过还是那句话,咱们应该手动指定类型,而不是让 Cython 编译器去猜,因为咱们是代码的编写者,类型什么的咱们本人最分明。因而 infer_types 这个装璜器,在工作中并不罕用,而且想进步速度,就必须当时显式地规定好变量的类型是什么。

小结

以上就是在 Cython 中如何动态申明一个变量,办法是应用 cdef 关键字。当时规定好类型是十分重要的,一旦类型确定了,那么生成的机器码的数量会少很多,从而实现速度的晋升。

而 C 类型的变量的运算速度比 Python 要快很多,这也是为什么 int 和 float 会抉择 C 的类型。而除了 int 和 float,C 的其它类型在 Cython 中也是反对的,包含指针、构造体、共同体这样的简单构造。

然而 C 的整型有一个问题,就是它是有范畴的,在应用的时候咱们要确保不会溢出。所以 Cython 在主动进行类型推断的时候,只有有可能扭转语义,就不会擅自应用 C 的整型,哪怕赋的整数十分小。这个时候能够通过 infer_types 装璜器,留给 Cython 更多的猜想空间。

不过还是那句话,咱们不应该让 Cython 编译器去猜,是否溢出是由咱们来确定的。如果能保障整数不会超过 int 所能示意的最大范畴,那么就将变量申明为 int;如果 int 无奈示意,那么就应用 long long;如果还无奈示意,那就没方法了,只能应用 Python 的整型了。而应用 Python 整型的形式就是不应用 cdef,间接动静申明即可。

所以如果要将变量申明为整型,那么间接应用 ssize_t 即可,等价于 long long。而在工作中,能超过 ssize_t 最大示意范畴的整数还是极少的。

# 须要确保赋给 a 的整数
# 不会超过 ssize_t 所能示意的最大范畴
cdef ssize_t a

# b 可能会十分十分大,也有可能是正数
# 甚至连 ssize_t 都无奈示意
# 此时就须要动静申明了,但很少会遇到这么大的整数
b = ...

再次强调,当时规定好类型对速度的晋升起着十分重要的作用。因而在申明变量的时候,肯定将类型指定好,特地是波及到数值计算的时候。只不过此时应用的是 C 的类型,须要额定思考整数溢出的状况,但如果将类型申明为 ssize_t 的话,还是很少会产生溢出的。

以上就是 cdef 的用法,以上就是本次分享的所有内容,想要理解更多 python 常识欢送返回公众号:Python 编程学习圈,发送“J”即可收费获取,每日干货分享

正文完
 0