第6篇:Cython的面向对象编程(中篇)
我们在前一篇已经说过Python版本实现的类和Cython版本的类的区别,其中一个最为显著的特征是Python类实例的属性数据存放一个内部字典中即__ dict__,我们了解dict的底层是基于哈系表
例如 Fruit的实例下,如下图查看所有实例属性的数据
ss8.png另外我在前面的随笔说过,纯Python版本的面向对象编程中,不存在封装一说,因为Python的类是基于解析语言构建,Python运行时系统并没有像C++/JAVA提供对类实例的属性/方法的访问控制
我们先来查看原生Python定义的Fruit类,我们可以通电点号访问符实访问Fruit实例的属性,同时可以修改其属性
因此Python定义的类默认所有属性是公开,并且可修改的,甚至Python运行时还允许我们动态添加新的类属性变量,如下操作,动态添加weight属性
Python类性能问题
上面的例子,其实就突出一点Python类强调运行时的动态性,因为Python类的实例属性存储内部dict中,而dict的本质就是哈系表,但纯Python类实例的属性数量规模不宜过大,因为哈系表元素(类实例属性的引用)的数量超过负载系数,会导致字典重散列,在典型的哈希表的内存模型中,重散列(Rehashing)意味着
- 带来巨大的内存消耗,因为重散列意味着,运行时频繁添加或删除类实例属性,可能导致已分配内存利用率低下
- 字典内部若类实例属性存在散列冲突,即访问或修改某些实例属性的效率时间复杂度可能为O(n)
扩展阅读:如果你对哈希表基本原理不熟悉,可以阅读我之前写的随笔《C++哈希表-负载系数与重散列》,那篇是非收费文章..
Cython类的访问控制
接下来,我们讨论Cython版本的类Fruit,这是一个纯Cython的扩展类,位于一个cy_fruit.pyx的文件中
#cython:language_level=3
cdef class Fruit(object):
'''Fruit Type'''
cdef str name
cdef double qty
cdef double price
def __init__(self,nm,qt,pc):
self.name=nm
self.qty=qt
self.price=pc
def amount(self):
return self.qty*self.price
下面是一个错误示例的演示
当我们尝试对一个C实现的扩展类的实例调用一个类似Python类的内部字典属性__ dict__会发生报错,会提示 no attribute '__ dict __'错误,
ss8.png
由于Cython关键字 cdef class就是告知Cython编译器将Fruit类的定义编译为C版本的类Fruit,若你了解C的话,C版本的Fruit类定义可以类比下面的C代码,
typedef struct{
char* name;
double qty;
double price;
} Fruit;
double amount(Fruit* self){
return self->qty*self->price;
}
没错,cdef class 在底层就做了这些底层的操作:
- 对于C来说,Fruit类的本体就是一个具备以上字段的结构体,
- Fruit类在实例化时,必须明确告知编译器它自己内部字段(属性)的类型,编译器根据字段的类型能计算出整个Fruit结构体实例化时该为它分配多大的内存量。
当然上面不是完全的描述,我们在通过例子去进一步分析cdef关键字的对Cython类的操作。
反例演示:当我们尝试通过点号访问符访问C级别的Fruit类实例属性,哦~!一一报错,这又是何解呢?你可以自行思考一下。
此时,你应该想到Cython的关键字cdef class集成了C++ class的特性就是类成员的访问控制,C++的关键字class就是提供了访问控制的struct的加强版。这里需要说一下:Cython与C++访问控制修饰符的对应关系。
- private:对于Cython编译器来说,任何使用cdef定义的类属性/方法默认是私有的,类外部代码无法访问之,并且在声明类成员时,Cython语法层面不提供显式的private关键字声明,因为像cdef private double price 等同画蛇舔足。
- public: Cython编译器继承了C++这一特性,例如类内声明cdef public double price,表示外部代码可以自由访问和修改该属性值。
- protected:要在Cython中实现类似C++类继承的protected访问控制特性,在Cython中不能使用protected,而是使用cppclass关键字,关于此方面内容以后再说。
再多说一句,我们知道C的struct是简单的聚合类型,也就是对基本数据类型聚合扩展新的用户自定义类型而已,且不提供任何访问限制。而Python 的C底层的API实现中使用最为频繁的函数参数就是struct PyObject*,只要Python解析器在运行时解析任何动态变量都会首先指向PyObject这个C版本的结构体指针。这恰好说明Python运行时系统对类实例属性没有访问控制的根本原因。
Ok,我们通过一些例子加深并巩固上面的概念,请看下面添加访问控制修饰符后的Cython类
#cython:language_level=3
cdef class Fruit(object):
'''Fruit Type'''
cdef readonly str name
cdef public double qty
cdef readonly double price
def __init__(self,nm,qt,pc):
self.name=nm
self.qty=qt
self.price=pc
cdef public double amount(self):
return self.qty*self.price
下面的调用Cython版本的Fruit类,类实例属性name和类实例属性qty都能被外部的Python代码调用,如下图,因为readonly和public允许外部Python代码访问其类属性
- 属性namereadonly关键字
- 属性qty被public关键字修饰
- 属性price被readonly关键修饰
值得一提的是,当readonly关键字修饰类属性时,其实很想Java中的final关键字的原理一样:类实例属性值一旦在初始化之后便不能更改,但允许调用代码访问。我们可以通过下面示例来演示。
public关键字允许外部Python代码调用被public修饰的类实例属性,并且可以自由修改类实例属性的值
更新中.....