类型转换
前面说了,动态类在继承扩展类、或者说静态类的时候,无法继承父类使用 cdef 定义的成员函数。因为动态类是 Python 一级的,而 cdef 定义的成员函数是 C 一级的,所以动态类的实例无法调用,因此也就没有跨语言的边界。
但我们可以通过类型转换实现这一点。
cdef class A: cdef funcA(self): return 123 class B(A): # B 是动态类,它的实例无法访问 C 一级的 cdef 方法 # 显然 func1 内部无法访问扩展类 A 的 funcA def func1(self): return self.funcA() # 但是我们在使用的时候将其类型转化一下 def func2(self): return (<A> self).funcA()
文件名叫 cython_test.pyx,编译测试一下:
import pyximport pyximport.install(language_level=3) import cython_test b = cython_test.B() try: b.func1() except Exception as e: print(e) """ 'B' object has no attribute 'funcA' """ # 动态类的实例, 无法调用父类的 cdef 方法 # 但 b.func2() 没有报错 # 因为内部在调用 funcA() 的时候进行了类型转换 print(b.func2()) # 123
在 func2 的内部我们将 self 转成了 A 的类型,所以它可以调用 funcA。
但我们知道对于 Python 类型而言,即使使用 <> 这种方式转化不成功,也不会有任何影响,会保留原来的值。而这可能有点危险,因此我们可以通过 (<A?> self) 进行转换,这样 self 必须是 A 或者其子类的实例对象,否则报错。
另外,如果使用 <> 进行转化的话,那么即使调用的是以双下划线开头的方法也是可行的。
cdef class A: cdef __funcA(self): return 123 class B(A): def func1(self): return self.__funcA() def func2(self): return (<A> self).__funcA()
这里的 func1 内部仍无法访问 __funcA,虽然我们知道动态类实例不能访问扩展类中使用 cdef 定义的方法,但真正的原因却不是这个。真正的原因是对于动态类实例而言,self.__funcA() 实际上会执行 self._B__funcA(),而这个方法没有。
但对于 func2 是可以的,我们在使用的时候将其类型转化一下,此时调用的就是 __funcA(),即便此时的名称以双下划线开头。
我们在前面说过,扩展类内部设置和获取属性(方法)时,不会在双下划线开头的名称前面加上 "_类名",其实说的还不够完善。如果一个对象是扩展类实例对象,那么即使不在扩展类的内部,其设置和获取属性(方法)时也不会在双下划线开头的名称前面加上 "_类名"。
比如这里的 func2,虽然是在动态类内部,但我们将其类型转成了扩展类型,所以在调用双下划线开头的方法时,是不会自动加上 "_类名" 的,所以此时仍然可以调用。
测试一下:
import pyximport pyximport.install(language_level=3) import cython_test b = cython_test.B() try: b.func1() except Exception as e: print(e) """ 'B' object has no attribute '_B__funcA' """ print(b.func2()) # 123
这里再介绍一个比较神奇的地方,我们来看一下:
cdef class Person: cdef str name cdef int age def __init__(self): self.name = "古明地觉" self.age = 16 cdef str get_info(self): # 注意: Person 的实例并没有 gender 属性 return f"name: {self.name}, " \ f"age: {self.age}, " \ f"gender: {self.gender}" class Girl(Person): def __init__(self): self.name = "satori" self.gender = "female" super().__init__() g = Girl() # g.get_info() 会报错 # 因为 get_info 是 cdef 定义的 # 这里将 g 的类型转化为 Person print((<Person?> g).get_info()) """ name: 古明地觉, age: 16, gender: female """
我们将 g 转成了 Person 类型之后,查找属性优先从 Person 实例里面查找,所以 self.name 得到的是 "古明地觉"。而如果某个属性 Person 的实例没有,那么再回到 Girl 的实例里面去找,比如 gender 属性。
所以这一点比较神奇,而方法也是同理。
cdef class A: cdef func3(self): return "A_func3" cdef __funcA(self): return self.func3(), self.func4() class B(A): def func2(self): return (<A> self).__funcA() def func3(self): return "B_func3" def func4(self): return "B_func4" b = B() print( b.func2() ) # ('A_func3', 'B_func4')
在调用 b.func2() 的时候,内部又调用了 __funcA(),但由于它是静态方法,所以需要类型转换,转换之后就是 A 的类型。然后 __funcA() 里面调用了 func3(),而 A 里面有 func3,所以直接调用;但是 func4 没有,于是再到 B 里面去找。因此最终返回 ('A_func3', 'B_func4')。
所以这一点可能有些绕:
b = B() print((<A ?> b).__class__) """ <class 'cython_test.B'> """
我们将 b 转成 A 类型之后再查看类型,发现显示的还是 B 的类型。但转化之后,之所以能够调用 A 的静态方法,原因就是 B 是 A 的子类,这里相当于将变量静态化了。转成 A 类型之后,优先查找 A 的属性,但实际类型仍然是 B,所以 A 里面找不到会去 B 里面找。
特殊的 None
看一个简单的函数。
cdef class Girl: cdef: str name int age def __init__(self, name, age): self.name = name self.age = age def dispatch(Girl g): # 这里的 g 是静态类型 # 即使不用 public 或 readonly 声明 # 也可以访问并修改内部的属性 return g.name, g.age
编译测试一下:
import pyximport pyximport.install(language_level=3) from cython_test import Girl, dispatch print(dispatch(Girl("古明地觉", 16))) print(dispatch(Girl("古明地恋", 15))) """ ('古明地觉', 16) ('古明地恋', 15) """ class _Girl(Girl): pass print(dispatch(Girl("雾雨魔理沙", 17))) """ ('雾雨魔理沙', 17) """ try: dispatch(object()) except TypeError as e: print(e) """ Argument 'g' has incorrect type (expected cython_test.Girl, got object) """
我们传递一个 Girl 或者其子类的实例对象的话是没有问题的,但是传递一个其它的则不行。
但是在 Cython 中 None 是一个例外,即使它不是 Girl 的实例对象,但也是可以传递的。除了 C 规定的类型之外,只要是 Python 的类型,不管什么,传递一个 None 都是可以的。这就类似于 C 中的空指针,任何的指针类型,我们都可以传递给空指针,只是没办法做什么操作。
所以这里可以传递一个 None,但是执行逻辑的时候显然会报错。
然而报错还是轻的,上面代码执行的时候会发生段错误,解释器直接异常退出了,而这里返回的状态码也很有意思。
首先每个进程退出的时候都有一个状态码,对于解释器而言,正常结束返回 0,出现异常返回 1。但如果出现上述这种很奇怪的状态码,说明是解释器内部出问题了,一般这种情况都是在和 C 交互的时候才有可能发生。
对于当前这个例子来说,原因就在于不安全地访问了 Girl 实例对象的成员属性,属性和方法都是 C 接口的一部分,而 Python 的 None 没有 C 接口,因此访问属性或者调用方法都是无效的。为了确保这些操作的安全,最好加上一层检测。
def dispatch(Girl g): if g is None: raise TypeError("g 不可以为 None") return g.name, g.age
但是除了上面那种做法,Cython 还提供了一种特殊的语法。
def dispatch(Girl g not None): return g.name, g.age
此时如果我们传递了 None,那么就会报错。不过这个版本由于要预先进行类型检查,判断是否为 None,从而会牺牲一些效率。不过虽说如此,但是传递 None 所造成的段错误是非常致命的,因此我们是非常有必要防范这一点的。
当然还是那句话,虽然效率会牺牲一点点,但与 Cython 带来的效率提升相比,这点牺牲是非常小的,况且这也是必要的。另外注意:not None 只能出现在 def 定义的函数中,cdef 和 cpdef 是不合法的。
此时对 None 也一视同仁,传递一个 None 也是不符合类型的。这里我们设置的是 not None,但是除了 None 还能设置别的吗?答案是不行,只能设置 None,因为 Cython 只有对 None 不会进行检测。
许多人认为 not None 字句的意义不大,这个特性经常被争论,但幸运的是,在函数的参数声明中使用 not None 是非常方便的。
个人觉得 Cython 的语法设计的真酷,笔者本人非常喜欢。