扩展类实例的类型转换,和关键字 None

简介: 扩展类实例的类型转换,和关键字 None


类型转换




前面说了,动态类在继承扩展类、或者说静态类的时候,无法继承父类使用 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 的语法设计的真酷,笔者本人非常喜欢。

相关文章
|
5月前
|
安全 编译器 程序员
特殊类设计以及C++中的类型转换
特殊类设计以及C++中的类型转换
52 2
|
5月前
|
编译器 C++
【C++11特性篇】新的类功能解读:新增加的[移动构造函数/移动赋值运算符重载]
【C++11特性篇】新的类功能解读:新增加的[移动构造函数/移动赋值运算符重载]
|
2天前
|
存储 Rust API
支持静态声明的类型
支持静态声明的类型
7 0
|
5月前
|
存储 算法 C++
【C/C++ 关键字 存储类说明符】 深入理解C/C++中的static关键字:它的作用、限制和使用技巧
【C/C++ 关键字 存储类说明符】 深入理解C/C++中的static关键字:它的作用、限制和使用技巧
30 0
|
5月前
|
存储 C# 索引
C#学习相关系列之数据类型类的定义(一)
C#学习相关系列之数据类型类的定义(一)
|
Java 编译器 C++
常量接口 vs 常量类 vs 枚举区别
把常量定义在接口里与类里都能通过编译,那2者到底有什么区别呢?
65 0
|
Java 编译器 数据库
Java维护常量方式的比较——接口、常量类与枚举
Java维护常量方式的比较——接口、常量类与枚举 一、示例 ​ 1.让类实现定义了常量的接口
|
存储 安全 Java
Java泛型-语法与常用实例
Java泛型-语法与常用实例
106 0
|
设计模式 安全 Java
C++特殊类的设计与类型转换
C++特殊类的设计与类型转换
C++ 继承与派生中的赋值兼容规则问题探究
C++ 继承与派生中的赋值兼容规则问题探究
170 0
C++ 继承与派生中的赋值兼容规则问题探究