楔子 前面我们介绍了 Cython 的语法,主要是一些基本的数据结构和函数,通过将静态类型引入到 Python 中,提升 Python 的执行效率。但 Cython 能做的事情还不仅如此,它还可以增强 Python 的类。 不过在了解细节之前,我们必须先了解动态类和静态类之间的区别,这样我们才能明白 Cython 增强 Python 类的做法是什么,以及它为什么要这么做。 动态类和静态类 我们知道 Python 一切皆对象,怎么理解呢?首先在最基本的层次上,一个对象有三样东西:地址、值、类型,通过 id 函数可以获取地址并将每一个对象都区分开来,通过 type 获取类型。至于对象的属性则放在自身的属性字典里面,这个字典可以通过 __dict__ 获取。而获取对象的某一个属性的时候,既可以通过 . 的方式来获取,也可以直接操作属性字典。 每一个对象都由一个类实例化得到,Python 也允许我们使用 class 关键字自定义一个类。使用 class 关键字定义的类,就叫做动态类。
动态类的属性可以被动态修改,解释器允许我们这么做,但是内置的类、和扩展类不行。
内置类和扩展类,统称为静态类,当然这两者本质上一样的,它们都是用 Python/C API 实现的。只不过前者已经由官方实现好了,内嵌在解释器里,比如 int, str, dict 等等,所以称之为内置类;而后者是我们根据业务逻辑,编写 C 扩展时手动实现的,所以叫扩展类,但它们没有什么本质上的区别,所以后面就用扩展类来描述了。 当操作扩展类的时候,操作的是编译好的静态代码,因此在访问内部属性的时候,可以实现快速的 C 一级的访问,这种访问可以显著的提高性能,这就是 Cython 要增强 Python 类的原因。 因为扩展类必须使用 Python/C API 在 C 的级别进行定义,但在 C 里面实现一个类、以及相关方法等等,这个过程很复杂,需要有专业的 Python/C API 知识。而麻烦的好处就是,扩展类的操作要比动态类高效很多。 而 Cython 则允许我们像实现动态类一样,去实现扩展类,这样既能拥有动态类的开发效率,又能有扩展类的运行效率。当然我们心里很清楚,用 Cython 实现的扩展类,和在 C 里面手动使用 Python/C API 实现的扩展类,效果上是一样的,因为 Cython 代码也是要被翻译成使用标准 Python/C API 的 C 代码,只不过这一步不需要我们手动做了。 下面来看看如何在 Cython 里面定义一个扩展类。 扩展类的定义 在 Cython 中定义一个扩展类通过 cdef class 的形式,和 Python 的动态类保持了高度的相似性。 尽管在语法上有着相似之处,但 cdef class 定义的扩展类对所有方法和数据都有快速的 C 级别的访问,这也是和扩展类和动态类之间的一个最显著的区别。而且扩展类和 int, str, list 等内置类都属于静态类,它们的属性默认不可修改。 我们先来写一个 Python 的类(动态类):
如果我们是对这个动态类编译的话,那么得到的类依旧是一个动态类,而不是扩展类。所有的操作,仍然是通过动态调度通用的 Python 对象来实现的。只不过由于解释器的开销省去了,因此效率上会提升一点点,但是它无法从静态类型上获益,因为此时的 Python 代码仍然需要在运行时动态调度来解析类型。 改成扩展类的话,我们需要这么做。
此时的关键字我们使用的是 cdef class,意思表示这个类不是一个普通的 Python 动态类,而是一个扩展类。并且在内部,我们还多了一个 cdef long width, height,它负责指定实例 self 所拥有的属性,因为扩展类实例不像动态类实例一样可以自由添加属性,静态类实例有哪些属性需要在类中使用 cdef 事先指定好。 这里的 cdef long width, height 就表示 Rectangle 实例只能有 width 和 height 两个属性、并且类型是 long,因此我们在实例化的时候,参数 w、h 只能传递整数。另外对于 cdef 来说,定义的类是可以被外部访问的,虽然函数不行、但类可以。 文件名叫 cython_test.pyx,我们编译测试一下:
注意:我们在 __init__ 中给实例绑定的属性,都必须在类中使用 cdef 声明,举个例子。
导入该文件,然后实例化的时候会报错:AttributeError: 'cython_test.Rectangle' object has no attribute 'height'。 凡是没有在类里面使用 cdef 声明的属性,都不可以访问,即使是赋值操作。也就是说,无论是获取还是赋值,self 的属性必须使用 cdef 在类里面声明。我们举一个Python 内置类型的例子:
扩展类和内置类是同级别的,无论是获取属性还是绑定属性,如果想通过 self. 的方式访问,那么一定要在类里面使用 cdef 声明。 所以扩展类无法动态绑定属性,扩展类有哪些属性在定义的时候就已经确定了。因为动态修改、添加属性,都是解释器在解释执行的时候动态操作的。而扩展类直接指向了 C 一级的结构,不需要解释器解释这一步,因此也失去了动态修改的能力。也正因为如此,才能提高效率,很多时候我们不需要动态修改。 另外当一个类实例化后,会给实例对象一个属性字典,通过 __dict__ 获取,它的所有属性以及相关的值都会存储在这里。其实获取一个实例对象的属性,本质上是从属性字典里面获取,instance.attr 等价于 instance.__dict__["attr"],同理修改、创建也是。但是注意:这只是针对动态类而言,而扩展类的实例对象是没有属性字典的。
原因很好想,因为动态类的实例可以自由添加属性,最合适的办法就是使用一个字典来存储。而扩展类的实例有哪些属性都是写死的,所以内部会使用数组保存,每个属性一个萝卜一个坑,按照顺序排好,在访问的时候是基于索引访问的,因此效率会更高,也更节省空间。
还是那句话,动态添加、删除属性,这些都是解释器在解释字节码的时候动态操作的,在解释的时候允许开发者做一些动态操作。但扩展类不需要解释这一步,它是彪悍的人生,编译之后直接指向了 C 一级的数据结构,因此也就丧失了这种动态的能力。 所以扩展类的实例没有属性字典,无法动态添加和删除属性。当然啦,虽然扩展类的实例没有属性字典,但是扩展类本身是有属性字典的,这一点和动态类一样。只是这个字典不允许修改,因为虽然叫属性字典,但它的类型实际上一个 mappingproxy。 mappingproxy 对象在底层就是对字典进行了一层封装,在字典的基础上移除了增删改操作,只保留了查询,查询 mappingproxy 对象本质上也是在查询内部的字典。 此外,默认情况下,扩展类实例的已有属性,外界也是不可访问的。
和之前的逻辑一样,我们测试一下。
我们看到没有 width 属性,height 也是同理,默认情况下,已有属性也不可被外界访问。但如果我们就是想修改 self 的已有属性呢?答案是将其暴露给外界即可。
通过 cdef public 声明的属性,是可以被外界获取并修改的,但是实例依旧没有属性字典,此时修改属性等价于修改数组元素。因为扩展类的实例有哪些属性是确定的,是通过数组静态存储的。 另外除了 cdef public 之外还有 cdef readonly,同样会将属性暴露给外界,但是只能访问不能修改。我们将代码中的 public 改成 readonly,然后再测试一下。
我们看到修改属性的时候报错了,告诉我们属性不可写。 所以扩展类的实例有哪些属性,需要在扩展类里面使用 cdef 提前声明好,实例对象在创建之后,这些属性就会顺序存储在数组中,不可以动态添加和删除。另外,即便是已有属性,根据声明方式的不同,也会有不同的表现。
当然创建实例对象无论是使用 cdef public 还是 cdef readonly,如果是在 Cython 里面创建的话,那么实例属性在任何情况下都是可以自由访问和修改的。因为 Cython 内部会屏蔽扩展类中的 readonly 和 public 声明,它们存在的目的只是为了控制来自外界(Python)的访问。 这里还有一点需要注意,当在类里面使用 cdef 声明变量的时候,其属性就已经绑定在 self 中了。我们举个栗子:
测试一下:
即便我们没有定义初始化函数,这些属性也是可以访问的,因为在使用 cdef 声明的时候,它们就已经绑定在上面了,只不过这些属性对应的值都是零值。 所以 self.xxx = ... 相当于是为绑定在 self 上的属性重新赋值,但赋值的前提是 xxx 必须已经是 self 的一个属性,否则是没办法赋值的。而 xxx 如果想成为 self 的一个属性,那么就必须在类里面使用 cdef 进行声明。 但是问题来了,这毕竟是在类里面声明的,那么类是否可以访问呢?
答案是可以访问,不过类访问没有太大意义,打印的结果只是告诉你这是实例的一个属性。 如果想设置类属性,不需要使用 cdef,而是像动态类一样去定义类属性。 在类里面使用 cdef 声明属性的时候不可以赋初始值(会有一个零值),否则编译时会报错,赋值这一步应该在初始化函数中完成。但不使用 cdef、而是像动态类一样定义常规类属性的话,是需要赋初始值的(这是显然的,否则就出现 NameError了)。 |
|