扩展类实例的类型转换,和关键字 None
类型转换
前面说了,动态类在继承扩展类、或者说静态类的时候,无法继承父类使用 cdef 定义的成员函数。因为动态类是 Python 一级的,而 cdef 定义的成员函数是 C 一级的,所以动态类的实例无法调用,因此也就没有跨语言的边界。
但我们可以通过类型转换实现这一点。
cdefclassA:
cdeffuncA(self):
return 123class
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 或者其子类的实例对象,否则报错。
另外,如果使用 <> 进行转化的话,那么即使调用的是以双下划线开头的方法也是可行的。
cdefclassA: cdef__funcA(self):
return 123
classB(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
这里再介绍一个比较神奇的地方,我们来看一下:
cdefclassPerson:
cdefstr name
cdefint 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 属性。
所以这一点比较神奇,而方法也是同理。
cdefclassA: 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
看一个简单的函数。
cdefclassGirl:
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):
passprint(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 的语法设计的真酷,笔者本人非常喜欢。