第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)意味着

  1. 带来巨大的内存消耗,因为重散列意味着,运行时频繁添加或删除类实例属性,可能导致已分配内存利用率低下
  2. 字典内部若类实例属性存在散列冲突,即访问或修改某些实例属性的效率时间复杂度可能为O(n),尤其是类继承中的属性访问会经历多个间接级别的查找操作后才能找到目标类属性,这时间开销甚至可能达到O(n^2)

扩展阅读:如果你对哈希表基本原理不熟悉,可以阅读我之前写的随笔《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 在底层就做了这些底层的操作:

  1. 对于C来说,Fruit类的本体就是一个具备以上字段的结构体,
  2. 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修饰的类实例属性,并且可以自由修改类实例属性的值

刚才我们仅讨论了Cython对类属性的访问控制,接下来讨论一下Cython访问修饰符对类方法的影响。仍然从一个示例来说明

我们从Cython版本的Fruit类实现中,知道Fruit类的方法amount的函数签名是

cdef public double amount(self)

但访问修饰符public对类方法不起作用,因为对于扩展类型的方法本质上忽略了readonly和public的声明,cdef关键字声明的类方法是本质上是以该类指针为传入参数的C函数

在这种情况,cdef关键字声明的类方法忽略了任何访问控制修饰符

  • cdef 声明的类方法对于Python外部代码是不可见的,仅有类内部的其他C级别类方法才能相互调用。
  • C级别下的类实例的方法可以自由访问类实例属性。

那我们需要为Python外部代码公开C级别的类方法接口,怎么做呢?没错,那就是使用cpdef关键字替换cdef关键字,即Fruit类方法amount的声明为

cpdef double amount(self)

cdef class Fruit(object):
    ......
    cpdef double amount(self):
          return  self.qty*self.price

因为cpdef关键字会在编译时,会生成一个对C版本的类方法amount调用的Python包装函数def amount(self),Python外部代码自然可以理解该函数,下面是cpdef声明的amount类方法等效的Cython代码,即

cdef class Fruit(object):
    ......
    cdef double _amount(self):
          return  self.qty*self.price
        
    def amount(self):
        return self._amount()

我们本篇详细讨论了Cython访问控制的原理,语法层面的知识,Cython的面向对象就是那么简单,只要你Python的基础扎实,语法是没什么好说的,若要理解Cython底层的操作,需要读者要有C和C++的背景知识,这也是很多国内写Cython编程技术随笔的程序员很少谈及Cython编译器的基本原理,只谈及如何令Python加速这些内容。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容