Cython基础教程(三)- Cython中的扩展类型

Cython中的扩展类型

考虑如下一个简单的类:

class Particle(object):
    def __init__(self, m, p, v):
        self.mass = m
        self.position = p
        self.velocity = v
    def get_momentum(self):
        return self.mass * self.velocity

上述这个类使用纯python定义的,可以被Cython编译,但是没有使用静态类型定义,没有任何性能的提升,可以将这个类进行如下改写:

cdef class Particle:
    cdef double mass, posotion, velocity
    def __init__(self, m, p, v):
        self.mass = m
        self.position = p
        self.velocity = v
    def get_momentum(self):
        return self.mass * self.velocity

cdef class语句告诉cython建立一个扩展类型而不是一般的python类。cdef
有两个地方做了改动,第一个是在定义类之前加了cdef关键词,第二个是使用静态类型cdef声明的类属性。类似于C++中定义类属性的方式。对于扩展类型,必须使用cdef来定义类属性,否则可能或报错(undeclared attribute)

属性类型及属性访问

在纯python定义的Particle类中,访问属性self.mass实际上最终是通过实例的__dict__来获取属性值。定义在cdef class扩展类型的方法可以访问实例中的所有属性,而且cython会将这些属性self.mass或者self.velocity转换成c结构体字段,可以线束的提高运行速度。如果要访问扩展类型实例属性的时候,需要Cython将属性设置为real-only, readable, 或者writeable。例如这个例子将三个属性都设为read-only

cdef class Particle:
    cdef readonly double mass, posotion, velocity
    def __init__(self, m, p, v):
        self.mass = m
        self.position = p
        self.velocity = v
    def get momentum(self):
        return self.mass * self.velocity

但是如果只讲mass设为read-only, 其他的属性继续保持私有属性, 定义如下:

cdef class Particle:
    cdef readonly double mass
    cdef double posotion, velocity
    def __init__(self, m, p, v):
        self.mass = m
        self.position = p
        self.velocity = v
    def get momentum(self):
        return self.mass * self.velocity

如果想让属性即可读,又可写,可以定义public属性

cdef class Particle:
    cdef public double mass
    cdef readonly double posotion
    cdef velocity
    def __init__(self, m, p, v):
        self.mass = m
        self.position = p
        self.velocity = v
    def get momentum(self):
        return self.mass * self.velocity

上述代码通过public让mass属性即可读,又可写。 让positive属性可读,velocity属性为私有

C-level Initialization and Finalization

Cython有一个特殊的方法_cinit_, 用来进行C-level的分配和初始化。对于之前定义的Particles扩展类,__init__可以完成这个工作,因为所有的字段都是double类型的变量,不需要C-level的分配。对于__init__方法,有可能在创建对象的时候被调用很多次,也有可能一次都没有调用。Cython保证__cinit__方法被调用一次,而且在__init__,__new__之前被调用。Cython将初始化的参数传入__cinit__方法中。例如

cdef class Matrix:
    cdef:
        unsigned int nrows, ncols
        double *_matrix
    def __cinit__(self, nr, nc):
        self.nrows = nr
        self.ncols = nc
        self._matrix = <double*>malloc(nr * nc * sizeof(double))
        if self._matrix == NULL:
            raise MemoryError()
    def __dealloc__(self):
        if self._matrix != NUll:
            free(self._matrix)

如果self._matrix在__init__方法中定义。有两种情况会导致错误。第一种情况是__init__方法没有被调用(可能发生在使用python类方法定义构造函数),或者是__init__被调用两次(可能在有类继承的时候)。使用__cinit__则不会出现这种情况。还可以定义__dealloc__来进行内存的回收。Cython保证__deallic__是使用一次(在程序结束的时候)

cdef和cpdef定义扩展类型的方法

可以使用def, cdef和cpdef来定义扩展类型的方法,但是注意不能使用cdef和cpdef定义非cdef类中(we cannot use cdef and cpdef to define methods on non-cdef class),这样做会导致编译错误.

cdef定义的方法和cdef函数类似,所有的参数都按原样传入,没有python到C的类型转换,所有性能相比于def定义的方法性能有提升。当然这也意味着cdef定义的方法只能在cython中使用,不能被外部python代码调用。

cpdef定义的方法非常有用,正如cpdef函数一样,既可以被外部的python代码调用,也可以被Cython调用。当然参类型和返回值类型必须可以转换成python,而且只允许集中类型(不允许指针类型).例如,用cpdef定义一个扩展类型的方法

cdef class Particle:
    cdef double mass, position, velocity
    cpdef double get_momentum(self):
        return self.mass * self.velocity

# we have a function
def add_moments(particles):
    total_mon = 0.0
    for particle in particles:
        total_mom += particle.get_momentum()
    return total_mom

上述这段代码可以被cython编译运行。调用add_moments()在长度为1000的列表中(列表里每个元素都是Particle对象)大约需要65ms。
当python调用Particle对象的get_momentum方法的时候,the get_momentum python wrapper is used, and the correct packing and unpacking from Python object to underlying Particle struct occurs automatically.
如果增加类型信息,那么Cython的运行速度回更快。

def add_momentums_typed(list particles):
    cdef:
        double total_mom = 0.0
        Particle particle
    for particle in particles:
        total_mom += particle.get_momentum()
    return total_mom

注意到上述例子将参数particles声明为list类型,total_mom声明为double类型,循坏的变量particle声明为Particle类对象。add_momentums_typed函数实际上不包含任何python的对象,运行相同长度1000的列表时,只需要7ms。进一步地,通过对比实验发现。将particle声明为Particle对象对性能有显著的提升,而将total_mom声明为double类型和将particles声明为list类型对最终性能没什么帮助

如果将get_momentum方法用cdef来定义呢,效果会有什么提升?看下面的例子:

cdef class Particle:
    cdef double mass, position, velocity
    cdef double get_momentum_c(self):
        return self.mass * self.velocity
        
def add_momentums_typed_c(list particles):
    cdef:
        double total_mom = 0.0
        Particle particle
    for particle in particles:
        total_mom += particle.get_momentum_c()
    return total_mom

这个版本有着最佳的性能,运行时间只需要4.6ms, 大概提升了40%。 缺点是get_momentum_c只能在Cython中被调用,而外部的pyhton代码不能调用。为了理解性能提升的原因,我们还要知道扩展类型的继承,子类及多态性。

继承和子类

扩展类型可以子类化单个基类型,基类本身必须是用C定义的类型或者扩展类型。如果基类是python类,那么扩展类在继承基类的时候,cython编译会报错。例如,考虑Particle的子类, 存储了particle的momentm而不需要重新计算。

cdef class CParticle(Particle):
    cdef double momentum
    def __init__(self, m, p, v):
        super(CParticle, self).__init__(m, p, v)
        self.momentum = self.mass * self.velocity
    cpdef double get_momentum(self):
        return self.momentum

因为CParticle也是Particle, 所以任何在使用Particle类的地方都可以替换为Cparticle类。在之前定义的add_momentums方法中,也可以传入一个包含CParticles实例的列表。
当然也可以用纯python的方式来子类化Particle:

class PyParticle(Particle):
    def __init__(self, m, p, v):
        super(PyParticle, self).__init__(m, p, v)
    def get_momentum(self):
        return super(Pyparticle, self).get_momentum()

PyParticle类不能使用任何C-level的属性或者cdef定义的方法。它可以覆盖Particle中def和cpdef定义的方法。当然也可以将一个包含PyParticle的列表传入add_momentums_typed,但是运行速度很慢。

Casting and Subclasses

当使用动态类型对象的时候,Cython不能在这个对象上使用任何C-level的数据和方法,所有的属性访问必须通过Python/C API,速度很慢。如果我们知道对象的类型,可以把它转换为静态类型,这样Cython就可以访问C-level的属性和方法。
有两种方法可以做这种转换:

  • 创建一个静态类型的变量并将动态变量赋值给静态变量
  • 使用Cython的casting运算符

例如对于动态对象p,我们知道它可能是Particle或者其子类的实例。但是Cython只认为他是一个Python对象,当Cython调用P的get_momentum方法是会查看p的_dict_, 有这个方法则执行,否则会报错。但是如果直接把它转换为Particle对象,那么调用get_momentum会更快。当然如果对象p不是Particle或者Particle的子类,或爆出TypeErrpr异常,所以这也更安全。

# 创建静态类型
cdef Particle static_p p
print(static_p.get_momentum())
print(static_p.velocity)

# casting
print(<Particle?>p).get_momentum)
print(<Particle?>p).velocity)

调用static_p.get_momentum会直接访问用cpdef定义的get_momentum方法,static_p也可以直接访问velocity私有变量,而p是不能直接访问的

扩展类型对象和None

考虑下面一个函数

def dispatch(Particle p):
    print(p.get_momentum())
    print(p.velocity)

如果我们个月dispatch传入一个Non-Particle对象,那么会抛出一个TypeError的异常

dispatch(Particle(1,2,3)) # ok
dispatch(CParticle(1,2,3)) # ok
dispatch(PyParticle(1,2,3)) # ok
dispatch(object() # TypeError

但是,Cython会对None进行特殊处理,及时它不是Particle实例,Cython允许它传入dispatch函数中。这个相当于C中的空指针,会导致segmentation fault或者更糟糕的错误

dispatch(None) # segmentation fault

造成segmentation fault错误的原因是,当None对象传入diapatch函数时,需要访问cpdef定义的函数get_momentum和私有属性velocity, 这些都是C接口。 而python的None对象没有C接口,所以这样调用访问是无效的,为了让操作更安全,需要先对参数p进行检查:

def dispatch(Particle p):
    if p is None:
        raise TypeError("...")
    print(p.get_momentum())
    print(p.velocity)
  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值