Cython基础教程(二) -变量和函数

cython变量的声明

CtypeCython cdef statement
Pointerscdef int *p, void **buf
Stack-allocted C arayscdef int arr[10], double points[20][10]
typedefd aliased typescdef size_t len
Compound types(structs and unions)cdef tm time_struct
Function Pointerscdef void (*f)(int, double)

cython中混合使用静态类型变量和动态类型变量

cython中允许同时使用静态类型的变量和动态类型的变量.例如假设我们有几个静态类型的C变量,想把这些变量放入一个python的元组中,在Cython中,可以进行如下定义:

cdef int a, b, c
tuple_of_ints = (a, b, c)

上面的例子之所以可行是因为C语言的int类型和python语言的int类型有着明显的对应关系,所以python可以自动进行转换。如果a, b, c是C指针的话,那么会行不通. 下表给出了内置的python类型和c/c++类型的对应关系
在这里插入图片描述

在python3中,所有的int型对象都是无限的(unlimited precision),当我们将python的int类型转换为C时,cython会检查是否溢出, 如果C类型不能表示python的正向,那么运行的时候会抛出OverFlowError的错误

静态声明python类型的变量

我们知道可以使用cdef来定义c类型的变量,但是也可以用cdef来静态的定义python内置类型的变量,例如list, tuple, dict,或者是扩展类型Numpy arrays。例如

cdef list particles, eggs
cdef dict events
cdef str name
cdef set unique_names

一些除法的模块需要单独提一下。C和python对于计算带符号正式操作的时候有很大的不同。例如-1 % 5在python中等于4 但是在C中等于-1。 当两个整数相除时,Python会检查分母是否为0,为0的话会抛出ZeroDivisionError的错误,而C语言没有这样的机制。Cython使用了Python的语法,即使是C静态类型定义的整数,如果要使用C的语法,可以使用cdivision模块,有集中使用方式,如下:

# cython: cdivision=True

或者是

cimport cython
@cython.cdivision(True)

def divides(int a, int b):
    return a / b

或者是

cimport cython

def divides(int a, int b):
    with cython.cdivision(True):
     return a / b

引用计数和静态字符串变量

python有一个特性是自动内存管理,而Cython通过直接引用计数来实现,Cython有个定期运行的自动垃圾回收机来帮助清理内存(with an automatic garbage collector that runs periodically to clean up unreachable reference cycles). 当同时处理静态类型变量和动态类型变量的时候,Cython的自动内存管理是有意义的,看下面的一个例子.

b1 = b"all men is mortal"
b2 = b"Socrates is a man"
cdef char *buf = b1 + b2

b1 + b2表达式是临时的Python bytes对象,(the assignment attempts to extract the temporary object’s char pointer using Cython’s automatic conversion rules,)因为b1+b2的结果是一个临时对象,所以上述代码运行会报错(Beacuse the temporary result of the addition is deleted immediately after it is created). 正确的方法应该这样

tmp = s1 + s2
cdef char *buf = tmp

或者使用静态类型定义

cdef bytes tmp = s1+ s2
cdef char *buf = tmp

We have to create a temporary bytes object so that Python dose not delete the string data, and we must ensure that the temporary object is maintained as long as the C char* buffer is required.

Cython三种类型的函数

带有def关键字的python函数

Cython支持标准用def定义的python函数,例如下面这个计算阶乘的函数

def py_fact(n):
    if n <= 1:
        return 1
    return n * py_fact(n-1)

这个python函数是有效的cython代码。在Cython中,参数n是动态类型的python变量,当调用py_fact函数的时候,必须传入一个python对象。当然,我们可以告诉Cython将n定义为C的整数类型来进行加速,当然可能会溢出(Python的整型是无限大)

def typed_fact(long n):
    if n <= 1:
        return 1
    return n * typed_fact(n-1)

当我们在cython中定义函数时,可以混合使用Python动态类型的参数和C静态类型的参数。在上述的例子中,静态定义typed_fact函数的参数不会有太多的性能提升。因为typed_fact是python函数,它的返回值是一个python整数类型,不是静态C的long类型, 所以我们要怎样做才能获取性能提升呢?我们可以在Cython中使用cdef关键词来定义C函数

带有cdef关键词的C函数

当我们使用cdef来定义一个函数时,这个函数用的是C的语法。cdef定义的函数的参数类型和返回类型都是静态类型,可以与C指针对象,结构体以及其他不能被强制转换为python类型的C类型一起使用。可以简单的理解为用cdef定义的函数就是使用cython句法定义的c函数。看下面一个例子

cdef long c_fact(long n):
    if n <= 1:
        return 1
    return n*c_fact(n-1)

c_fact()函数和之前定义的typed_fact()函数最大的区别在于,c_fact()函数的参数类型和返回值类型都是声明的静态类型,没有任何的python对象,所以没有python类型到C类型的转换。调用c_fact()和调用纯C定义的阶乘函数是一样有效的。
使用cdef定义的函数可以被同一个源文件下的def函数和cdef函数调用。但是Cython不允许一个cdef定义的函数被外部python代码直接调用。由于这个限制,cdef函数通常用作辅助函数来帮助def函数完成它们的工作。例如,我们想在外部调用c_fast()函数,可以用def来定义一个函数调用c_fact():

def wrap_c_fast(n):
    return c_fact(n)

大家可以动手做实验来对比下wrap_c_fact(20)和typed_fact(20),py_fact(20)的时间,可以看出wrap_c_fact(20)运行时间远远短于其他两个函数.
具体的实现如下:
step1: 新建cython文件,test.pyx

cimport cython

@cython.cdivision(True)
def divides(int a, int b):
  return a / b

def py_fact(n):
  if n < 1:
    return 1
  return n * py_fact(n-1)

def typed_fact(long n):
  if n < 1:
    return 1
  return n * typed_fact(n-1)

cdef long c_fact(long n):
  if n < 1:
    return 1
  return c_fact(n-1)

def wrap_c_fast(n):
  return c_fact(n)

step2: 新建python文件,setup.py

from distutils.core import setup, Extension
from Cython.Build import cythonize

ext=Extension(name='fact', sources=['test1.pyx'])
setup(ext_modules=cythonize(ext))

在terminal中运行 python setup.py -build_ext --inplace

step3: 新建compare.py

import time
from functools import wraps
import fact


def time_wrapper(func):
  @wraps(func)
  def measure_time(*args, **kwargs):
    t1 = time.time()
    result = func(*args, **kwargs)
    t2 = time.time()
    print(f"{func.__name__} took {t2 - t1} seconds")
    return result
  return measure_time

@time_wrapper
def py_fact(n):
  return fact.py_fact(n)

@time_wrapper
def typed_fact(n):
  return fact.typed_fact(n)

@time_wrapper
def c_fact(n):
  return fact.wrap_c_fast(n)

if __name__ == "__main__":
  n = 20
  py_fact(n)
  typed_fact(n)
  c_fact(n)

"""
py_fact took 3.814697265625e-06 seconds
typed_fact took 2.86102294921875e-06 seconds
c_fact took 9.5367431640625e-07 seconds
"""

从结果上可以看出,使用c_fact()运行速度最快

使用cpdef函数将def和cdef进行结合

用cpdef定义的函数是cdef和def的混合体。在Cython中,为了让cdef定义的c_fact()函数可以被python使用,我们用def定义个一个Wrapper function. 然而使用cpdef定义的函数结合了两者的优点,We get a C-Only version of the function and a Python Weapper for it, both with the same name. When we call the function from Cython, we call the C-only version; When we call the function from Python, the wrapper is called. In this Way, cpdef functions combine the accessibility of def functions with the performance of cdef functions.
如下:

cpdef long cp_fact(long n):
    if n <= 1:
        return 1
    return n*cp_fact(n-1)

cpdef函数有一个限制,因为它同时做了python函数和c函数的工作,它的参数类型和返回值类型必须和Python和C兼容。任何python的内置类型都可以用c进行表示,但是C的有些类型,例如void, 指针类型,或者C数组不能用python表示。具体可以看上述的一张表。

函数和异常值处理

在Cython定义的一些函数,例如用cdef和cpdef定义的函数,可能返回的并不是一个python类型的对象,在函数运行报错的时候,可能被抛出一些异常。这个时候我们需要一些机制来处理这些异常情况。看下面一个例子:

cpdef int divide_ints(int i, int j):
    return i / j

如果给divide_ints的参数j=0的时候,会有一个ZeroDivisionError的异常。但是对于divide_ints()函数来说,无法抛出这个异常,这个时候需要特殊的机制。

>>> divide_ints(1,1)
1
>>> divide_ints(1,0)
Exception ZeroDivisionError: 'interger division or module by zero' in 'division.divide_ints' ignored
0

运行divide_ints(1,0)的时候警告信息表明这个错误被忽视了,而且给出了错误的结果。但对于Python来说,可以检测到ZeroDivisionError。为了处理这种异常,Cython给出了except关键字来允许cdef或cpdef的函数可以调用python的异常处理模块

cdef int divide_ints(int i, int j) except? -1:
    return i / j

except? -1表明如果函数抛出了异常,那么返回值为-1. 这里的-1是任意值,也可以使用其他的数来表示

强制类型转换

python语言和C语言都有强制类型转换的规则. Cython提供了一种非常类似于C语言的类型转换方法,差异在于把括号改成尖括号。例如下面从 void* 转换为 int*的例子

cdef int *ptr_i = <int*> v

相当于C中的

int *ptr = (int*) v

我们也可以用来转换一些python的内置类型或自定义的类型。例如:

def cast_to_list(a):
    cdef list cast_list = <list>a
    print(type(a))
    print(type(cast_list))
    cast_list.append(1)

在上面这个例子中,我们将任意一个python类型的对象转换为静态的列表。如果参数a是一个列表或者列表的子类,那么该函数会运行成果,否则会抛出一个异常。当我们不太确定参数的类型的时候,我们可以使用checked casting operator。例如

def cast_to_list(a):
    cdef list cast_list = <list?>a
    print(type(a))
    print(type(cast_list))
    cast_list.append(1)

当参数a不是列表或者列表子类型的时候,会抛出一个TypeError的异常。

声明和使用结构体,共用体,枚举类型

Cython同样可以使用C语言中的结构体、共用体及枚举类型。在C语言中,声明一个结构体或共用体:

struct mycpx {
    int a;
    float b;
};

union uu {
    int a;
    short b, c
}

在cython中,可以进行如下声明:

cdef struct mycpx:
    float real
    float imag
    
cdef union uu:
    int a
    short a,b,c

cythpn的语法是使用cdef和缩进来声明结构体和共用体。这也是Cython融合python和C的一个地方。
我们可以使用ctypedef关键词来定义一个类型别名,例如:

ctypedef struct mycpx:
    float real
    float imag
    
ctypedef union uu:
    int a
    short a,b,c

这样,定义一个结构体类型的变量可以表示为:

cdef mypyx zz

可以使用三种方式来初始化一个结构体

cdef mycpx a = mycpx(3.14, -1.0)
cdef mypyx b = mycpx(real=2.718, imag=1.618)

或者

cdf mypyx zz
zz.real = 3.14
zz.imag = -1.0

或者使用一个python的字典进行初始化。cython会对每一个赋值进行自动转换。

cdef mypyx zz = {'real':3.14, 'imag':-1.0}

对于枚举类型,可以用多行代码定义成员变量或者用一行代码。枚举类型可以用cdef或者ctypedef来定义。就像之前定义结构体一样。

cdef enum PRIMARIES:
    RED = 1
    YELLOW = 3
    BLUE = 5
    
cdef enum SECONDARIES:
    ORANGE, GREEN, PURPLE

用ctypedef定义类型别名

cython可以用ctypedef关键词来支持C中类型别名。和C语言中的使用方式很相似。下面是一个简单的例子

ctypedef double real
ctypedef long integral

def dislayment(real d0, real d1, real a, real t):
    cdef real d = d0 + (v0 * t) + (0.5 * a * t**2)
    return d

ctypedef功能对于C++来说非常有用,尤其是可以使用typedef来简化很长的模板类型的时候。注意,ctypedef不能再函数内部使用,而且必须在文件中出现才能使用。

Cython预处理器

Cython用DEF关键词来定义宏,运行的时候当成一个常量,类似于c预处理器#define. 对于一些很长的数字,有些时候非常有用,例如

DEF E = 2.71828182845904
DEF PI = 3.141592653589793

def test():
    return E * PI + 1.0
  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值