Cython 系列 4.定义和使用 Cython 中的扩展类,分析它们与 Python 中普通类的区别。
楔子
上一篇博客中,我们介绍 Cython 给 Python 赋予的一些额外特性,以及这些特性的使用方式,但那主要是基本的数据类型和函数。Cython 也可以增强 Python 的类,不过在了解细节之前,我们必须首先了解 Python 类和扩展类之间的区别,这样我们才能明白 Cython 增强 Python 类的做法是什么,以及它为什么要这么做。
Python 类和扩展类之间的差异
首先 Python 中 "一切皆对象",怎么理解呢?首先在最基本的层次上,一个对象有三样东西:地址、值、类型,我们通过 id 函数可以获取地址并将每一个对象都区分开来,使用 type 获取类型。Python 中对象有很多属性,这些属性都放在自身的属性字典里面,这个字典可以通过 __dict__
获取。我们调用对象的某一个属性的时候,可以通过 .
的方式来调用,Python 也允许我们通过 class 关键字自定义一个类。
class A:
pass
print(A.__name__) # A
A.__name__ = "B"
print(A.__name__) # B
try:
int.__name__ = "INT"
except Exception as e:
# 内建类型 和 扩展类型 不允许修改属性
print(e) # can't set attributes of built-in/extension type 'int'
正如之前说的那样,我们除了在 Python 中定义类,还可以直接使用 Python/C API 在 C 级别创建自己的类型,这样的类型称之为扩展类、或者扩展类型(说白了在 C 中实现的类就叫做扩展类)。
Python 解释器本来就是 C 写的,所以我们可以在 C 的层面上面实现 Python 的任何对象,类也是如此。Python 中自定义的类和内置的类在 C 一级的结构是一致的,所以我们只需要按照 Python/C API 提供的标准来编写即可。但还是那句话,使用 C 来编写会比较麻烦,因为本质上就是写 C 语言。
当我们操作扩展类的时候,我们操作的是编译好的静态代码,因此在访问内部属性的时候,可以实现快速的 C 一级的访问,这种访问可以显著的提高性能。但是在扩展类的实现、以及处理相应的实例对象和在纯 Python 中定义类是完全不同的,需要有专业的 Python/C API 的知识,不适合新手。
这也是 Cython 要增强 Python 类的原因:Cython 使得我们创建和操作扩展类就像操作 Python 中的类一样。在Cython中定义一个扩展类通过 cdef class 的形式,和 Python 中的常规类保持了高度的相似性。
尽管在语法上有着相似之处,但是 cdef class 定义的类对所有方法和数据都有快速的 C 级别的访问,这也是和扩展类和 Python 中的普通类之间的一个最显著的区别。而且扩展类和 int、str、list 等内置的类都属于静态类,它们的属性是不可修改的。
Cython 中的扩展类
写一个 Python 中的类吧。
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def get_area(self):
return self.width * self.height
这个类是在 Python 级别定义的,可以被 CPython 编译的。我们定义了矩形的宽和高,并且提供了一个方法,计算面积。这个类是可以动态修改的,我们可以指定任意的属性。
如果我们是对这个 Python 类编译的话,那么得到的类依旧是一个纯 Python 类,而不是扩展类。所有的操作,仍然是通过动态调度通用的 Python 对象来实现的。只不过由于解释器的开销省去了,因此效率上会提升一点点,但是它无法从静态类型上获益,因为此时的 Python 代码仍然需要在运行时动态调度来解析类型。
改成扩展类的话,我们需要这么做。
# cython_test.pyx
cdef class Rectangle:
cdef long width, height
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
此时的关键字我们使用的是 cdef class,意思就是表示这个类不是一个普通的 Python 类,而是一个扩展类。并且在内部,我们还多了一个 cdef long width, height
,它负责指定实例 self 所拥有的属性,因为静态类实例不像动态类实例一样可以*添加属性,静态类实例有哪些属性需要在类中使用 cdef 事先指定好。这里的 cdef long width, height
就表示 Rectangle 实例只能有 width 和 height 两个属性、并且类型是 long,因此我们在实例化的时候,参数 w、h 只能传递整数。另外对于 cdef 来说,定义的类是可以被外部访问的,虽然函数不行、但类可以。
import pyximport
pyximport.install(language_level=3)
import cython_test
rect = cython_test.Rectangle(3, 4)
print(rect.get_area()) # 12
try:
rect = cython_test.Rectangle("3", "4")
except TypeError as e:
print(e) # an integer is required
注意:我们在 __init__ 中实例化的属性,都必须在类中使用 cdef 声明,举个栗子。
cdef class Rectangle:
# 这里我们只声明了width, 没有声明height, 那么是不是意味着这个height可以接收任意对象呢?
cdef long width
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
import pyximport
pyximport.install(language_level=3)
import cython_test
rect = cython_test.Rectangle(3, 4)
"""
File "cython_test.pyx", line 7, in cython_test.Rectangle.__init__
self.height = h
AttributeError: 'cython_test.Rectangle' object has no attribute 'height'
"""
凡是在没有在 cdef 中声明的,都不可以赋值给 self,可能有人发现了这不是访问,而是添加呀。我添加一个属性咋啦,没咋,无论是获取还是赋值,self 中的属性必须使用 cdef 在类中声明。我们举一个Python 内置类型的例子吧:
a = 1
try:
a.xx = 123
except Exception as e:
print(e) # 'int' object has no attribute 'xx'
一样等价,我们的扩展类和内建的类是同级别的,一个属性如果想通过 self.
的方式来调用,那么一定要在类里面通过 cdef 声明。
cdef class Rectangle:
cdef long width, height
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
import pyximport
pyximport.install(language_level=3)
import cython_test
rect = cython_test.Rectangle(3, 4)
try:
rect.a = "xx"
except AttributeError as e:
print(e) # 'cython_test.Rectangle' object has no attribute 'a'
"""
如果想动态修改、添加属性,那么需要解释器在解释的时候来动态操作
但扩展类和内置的类是等价的,直接指向了C一级的结构,不需要解释器解释这一步,因此也失去了动态修改的能力
也正因为如此,才能提高效率。因为很多时候,我们不需要动态修改。
当一个类实例化之后,会给实例对象一个属性字典,通过__dict__获取,它的所有属性以及相关的值都会存储在这里
其实获取一个实例对象的属性,本质上是从属性字典里面获取,instance.attr 等价于instance.__dict__["attr"],同理修改、创建也是。
但是注意:这只是针对普通的 Python 类而言,但扩展类的实例对象内部是没有 __dict__ 的。
"""
try:
rect.__dict__
except AttributeError as e:
print(e) # 'cython_test.Rectangle' object has no attribute '__dict__'
# 不光 __dict__, 你连 self 本身的属性都无法访问
try:
rect.width
except AttributeError as e:
print(e) # 'cython_test.Rectangle' object has no attribute 'width'
# 提示我们 self 没有 width 属性,所以我们实例化之后再想修改是不行的,连获取都获取不到
# 只能调用它的一些方法罢了。
所以内建的类和扩展类是完全类似的,其实例对象都没有属性字典,至于类本身是有属性字典的,但是这个字典不可修改。因为虽然叫属性字典,但它的类型实际上是一个 mappingproxy。
import pyximport
pyximport.install(language_level=3)
import cython_test
try:
int.__dict__["a"] = 123
except TypeError as e:
print(e) # 'mappingproxy' object does not support item assignment
try:
cython_test.Rectangle.__dict__["a"] = 123
except TypeError as e:
print(e) # 'mappingproxy' object does not support item assignment
还是那句话,动态设置、修改、获取、删除属性,这些都是在解释器解释字节码的时候动态操作的,在解释的时候允许你做一些这样的骚操作。但是内置的类和扩展类不需要解释这一步,它们是彪悍的人生,直接指向了 C 一级的数据结构,因此也就丧失了这种动态的能力。
但是扩展类毕竟是我们自己指定的,如果我们就是想修改 self 的一些属性呢?答案是将其暴露给外界即可。
cdef class Rectangle:
# 通过cdef public的方式进行声明即可
# 这样的话就会暴露给外界了
cdef public long width, height
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
import pyximport
pyximport.install(language_level=3)
import cython_test
rect = cython_test.Rectangle(3, 4)
print(rect.width) # 3
print(rect.get_area()) # 12
rect.width = 123
print(rect.get_area()) # 492
try:
rect.__dict__
except AttributeError as e:
print(e) # 'cython_test.Rectangle' object has no attribute '__dict__'
# 属性字典依旧是没有的
通过 cdef public 声明的属性,是可以被外界获取并修改的,除了 cdef public 之外还有 cdef readonly,同样会将属性暴露给外界,但是只能访问不能修改。
cdef class Rectangle:
cdef readonly long width, height
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
import pyximport
pyximport.install(language_level=3)
import cython_test
rect = cython_test.Rectangle(3, 4)
print(rect.width) # 3
try:
rect.width = 123
except AttributeError as e:
print(e) # attribute 'width' of 'cython_test.Rectangle' objects is not writable
cdef readonly 类型 变量名:实例属性可以被外界访问,但是不可以被修改
cdef public 类型 变量名:实例属性即可以被外界访问,也可以被修改
cdef 类型 变量名:实例属性既不可以被外界访问,更不可以被修改
当然定义变量无论是使用 cdef public 还是 cdef readonly,如果是在 Cython 里面实例化的话,内部实例属性在任何情况下都是可以*访问和修改的。因为 Cython 内部会屏蔽扩展类中的 readonly 和 public 的声明,它们存在的目的只是为了控制来自外界(Python)的访问。
这里还有一点需要注意,当在类里面使用 cdef 声明变量的时候,其属性就已经绑定在 self 中了。我们举个栗子:
cdef class Rectangle:
cdef public long width, height
cdef public float area
cdef public list lst
cdef public tuple tpl
cdef public dict d
import pyximport
pyximport.install(language_level=3)
import cython_test
rect = cython_test.Rectangle()
print(rect.width) # 0
print(rect.height) # 0
print(rect.area) # 0.0
print(rect.lst) # None
print(rect.tpl) # None
print(rect.d) # None
即便我们没有定义初始化函数,这些属性也是可以访问的,因为在使用 cdef 声明的时候,它们就已经绑定在上面了,而这一步显然发生在编译阶段,只不过这些属性对应的值都是零值。所以 self.xxx = ...
相当于是为绑定在 self 上的属性重新赋值,但赋值的前提是 xxx 必须已经是 self 的一个属性,否则是没办法赋值的,而 xxx 如果想成为 self 的一个属性那么就必须在类里面使用 cdef 进行声明。
但是问题来了,这毕竟是在类里面声明的,那么类是否可以访问呢?
import pyximport
pyximport.install(language_level=3)
import cython_test
print(cython_test.Rectangle.width) # <attribute 'width' of 'cython_test.Rectangle' objects>
答案是可以访问,不过类访问没有太大意义,打印的结果只是告诉你这是实例的一个属性。如果想设置类属性,就不需要使用 cdef,而是像动态类一样去定义类属性。并且在类里面使用 cdef 的声明属性的时候不可以赋初始值,否则报错,赋值这一步应该在初始化函数中完成。但不使用 cdef、而是像动态类一样定义常规类属性的话,是可以赋初始值的(这是显然的,否则就出现 NameError了)。
C 一级的构造函数和析构函数
每一个实例对象都对应了一个 C 结构体,其指针就是 Python 调用 __init__ 函数里面的 self 参数。当 __init__ 参数被调用时,会初始化 self 参数上的属性,而且 __init__ 参数是自动调用的。但是我们知道在 __init__ 参数调用之前,会先调用 __new__ 方法, __new__ 方法的作用就是为创建的实例对象开辟一份内存,然后返回其指针并交给 self。在 C 级别就是,在调用 __init__ 之前,实例对象指向的结构体必须已经分配好内存,并且所有结构字段都处于可以接收初始值的有效状态。
Cython 扩充了一个名为 __cinit__ 的特殊方法,用于执行 C 级别的内存分配和初始化。不过对于之前定义的 Rectangle 类的 __init__ 方法,因为内部的字段接收的值是两个 double,不需要 C 级别的内存分配。但如果需要 C 级别的内存分配,那么就不可以使用 __init__ 了,而是需要使用 __cinit__。
# 导入相关函数,malloc,free
# 如果不熟悉的话,建议去了解一下C语言
from libc.stdlib cimport malloc, free
cdef class A:
cdef:
unsigned int n
double *array # 一个数组,存储了double类型的变量
def __cinit__(self, n):
self.n = n
# 在C一级进行动态分配内存
self.array = <double *>malloc(n * sizeof(double))
if self.array == NULL:
raise MemoryError()
def __dealloc__(self):
"""如果进行了动态内存分配,也就是定义了 __cinit__,那么必须要定义 __dealloc__
否则在编译的时候会抛出异常:Storing unsafe C derivative of temporary Python reference
然后我们释放掉指针指向的内存
"""
if self.array != NULL:
free(self.array)
def set_value(self):
cdef long i
for i in range(self.n):
self.array[i] = (i + 1) * 2
def get_value(self):
cdef long i
for i in range(self.n):
print(self.array[i])
import pyximport
pyximport.install(language_level=3)
import cython_test
a = cython_test.A(5)
a.set_value()
a.get_value()
"""
2.0
4.0
6.0
8.0
10.0
"""
所以 __cinit__ 是用来进行 C 一级内存的动态分配的,另外我们说如果在 __cinit__ 通过 malloc 进行了内存分配,那么必须要定义 __dealloc__ 函数将指针指向的内存释放掉。当然即使我们不释放也没关系,只不过可能发生内存泄露(雾),但是 __dealloc__ 这个函数是必须要被定义,它会在实例对象回收时被调用。
这个时候可能有人好奇了,那么 __cinit__ 和 __init__ 函数有什么区别呢?区别还是蛮多的,我们细细道来。
首先它们只能通过 def 来定义,另外在不涉及 malloc 动态分配内存的时候, __cinit__ 和 __init__ 是等价的。然而一旦涉及到 malloc,那么动态分配内存只能在 __cinit__ 中进行,如果这个过程写在了 __init__ 函数中,比如将我们上面例子的 __cinit__ 改为 __init__ 的话,你会发现 self 的所有变量都没有设置进去、或者说设置失败,并且其它的方法若是引用了 self.array,那么还会导致丑陋的段错误。
还有一点就是,__cinit__ 函数会在 __init__ 函数之前调用,我们实例化一个扩展类的时候,参数会先传递给 __cinit__,然后 __cinit__ 再将接收到的参数原封不动的传递给 __init__。
cdef class A:
cdef public:
unsigned int a, b
def __cinit__(self, a, b):
print("__cinit__")
self.a = a
self.b = b
print(self.a, self.b)
def __init__(self, c, d):
"""__cinit__ 中接收两个参数
然后会将参数原封不动的传递到这里,所以这里也要接收两个参数
参数名可以不一致,但是个数要匹配
"""
print("__init__")
print(c, d)
import pyximport
pyximport.install(language_level=3)
import cython_test
a = cython_test.A(111, 222)
"""
__cinit__
111 222
__init__
111 222
"""
print(a.a) # 111
print(a.b) # 222
注意:__cinit__ 只有在涉及 C 级别内存分配的时候才会出现,如果没有涉及那么使用 __init__ 就可以,虽然在不涉及 malloc 的时候这两者是等价的,但是 __cinit__ 会比 __init__ 的开销要大一些。而如果涉及 C 级别内存分配,那么建议 __cinit__ 只负责内存的动态分配,__init__ 负责属性的创建。
from libc.stdlib cimport malloc, free
cdef class A:
cdef public:
unsigned int a, b, c
# 这里的 array 不可以使用 public 或者 readonly
# 原因很简单,因为一旦指定了 public 和 readonly,就意味着这些属性是可以被 Python 访问的
# 所以需要其能够转化为 Python 中的对象,而 C 中的指针,除了 char *, 都是不能转化为 Python 对象的
# 因此这里的 array 一定不能暴露给外界,否则编译出错,提示我们:double * 无法转为 Python 对象
cdef double *array
def __cinit__(self, *args, **kwargs):
# 这里面只做内存分配,设置属性交给__init__
self.array = <int *>malloc(3 * sizeof(int))
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def __dealloc__(self):
free(self.array)
我们上面使用了 malloc 函数进行内存动态申请、free 函数进行内存释放,但是相比 malloc、free 这种 C 级别的函数,Python 提供了更受欢迎的用于内存管理的函数,这些函数对较小的内存块进行了优化,通过避免昂贵的操作系统调用来加快分配速度。
from cpython.mem cimport PyMem_Malloc, PyMem_Realloc, PyMem_Free
cdef class AllocMemory:
cdef double *data
def __cinit__(self, size_t number):
# 等价于 C 的 malloc
self.data = <double *> PyMem_Malloc(sizeof(double) * number)
if self.data == NULL:
raise MemoryError("内存不足,分配失败")
print(f"分配了 {sizeof(double) * number} 字节的内存")
def resize(self, size_t new_number):
# 等价于 C 的 realloc,一般是容量不够了才会使用
# 相当于是申请一份更大的内存,然后将原来的 self.data 里面的内容拷过去
# 如果申请的内存比之前还小,那么内容会发生截断
mem = <double *> PyMem_Realloc(self.data, sizeof(double) * new_number)
if mem == NULL:
raise MemoryError("内存不足,分配失败")
self.data = mem
print(f"重新分配了 {sizeof(double) * new_number} 字节的内存")
def __dealloc__(self):
"""定义了 __cinit__,那么必须定义 __dealloc__"""
if self.data != NULL:
PyMem_Free(self.data)
print("内存被释放")
Python 提供的这些内存分配、释放的函数和 C 提供的原生函数,两者的使用方式是一致的,事实上 PyMem_* 系列函数只是在 C 的 malloc、realloc、free 基础上做了一些简单的封装。但不管是哪种,一旦分配了,那么就必须要进行释放,否则只有等到 Python 进程退出之后它们才会被释放,这种情况便称之为内存泄漏。
import pyximport
pyximport.install(language_level=3)
import cython_test
alloc_memory = cython_test.AllocMemory(50)
alloc_memory.resize(60)
del alloc_memory
print("--------------------")
"""
分配了 400 字节的内存
重新分配了 480 字节的内存
内存被释放
--------------------
"""
我们看到是没有任何问题的,因此以后在涉及动态内存分配的时候,建议以后在工作中使用 PyMem_* 系列函数。当然后面为了演示方便,我们还是使用 malloc 和 free。
cdef和cpdef方法
我们之前使用了 cdef 和 cpdef,我们说:cdef 可以定义变量和函数,但是不能被 Python 直接访问;可以定义一个类,能直接被外界访问。而 cpdef 专门用于定义函数,cpdef 定义的函数既可以在 Cython 内部访问,也可以被外界访问,因为它定义了两个版本的函数:一个是高性能的纯C版本(此时等价于 cdef,至于为什么高效,因为它是 C 一级的,直接指向了具体数据结构,当然还有其它原因,我们之前都说过的),另一个是 Python 包装器(相当于我们手动定义的 Python 函数),所以我们还要求使用cpdef定义的函数的参数和返回值类型必须是 Python 可以表示的,像 char * 之外的指针就不行。
那么同理它们也可以作用于方法,当然方法也是实例对象在获取函数的时候进行封装得到的,所以一样的道理。但是注意:cdef 和 cpdef 只能修饰 cdef class 定义的静态类里面的方法,如果是 class 定义的纯 Python 类,那么内部是不可以出现 cdef 或者 cpdef 的。
cdef class A:
cdef public:
long a, b
def __init__(self, a, b):
self.a = a
self.b = b
cdef long f1(self):
return self.a * self.b
cpdef long f2(self):
return self.a * self.b
import pyximport
pyximport.install(language_level=3)
import cython_test
a = cython_test.A(11, 22)
print(a.f2()) # 242
a.f1()
"""
a.f1()
AttributeError: 'cython_test.A' object has no attribute 'f1'
"""
cdef 和 cpdef 之间在函数上的差异,在方法中得到了同样的体现。
此外,这个类的实例也可以作为函数的参数,这个是肯定的。
cdef class A:
cdef public:
long a, b
def __init__(self, a, b):
self.a = a
self.b = b
cpdef long f2(self):
return self.a * self.b
def func(self_lst):
s = 0
for self in self_lst:
s += self.f2()
return s
import pyximport
pyximport.install(language_level=3)
import cython_test
a1 = cython_test.A(1, 2)
a2 = cython_test.A(2, 4)
a3 = cython_test.A(2, 3)
print(cython_test.func([a1, a2, a3])) # 16
这是 Python 的特性,一切都是对象,尽管没有指明 self_lst 是什么类型,但只要它可以被 for 循环即可;尽管没有指明 self_lst 里面的元素是什么类型,只要它有 f2 方法即可。并且这里的 func 可以在 Cython 中定义,同样可以在 Python 中定义,这两者是没有差别的,因为都是 Python 中的函数。另外在遍历的时候仍然需要确定这个列表里面的元素是什么,意味着列表里面的元素仍然是 PyObject *,它需要获取类型、转化、属性查找,因为 Cython 不知道类型是什么、导致其无法优化。但如果我们规定了类型,那么再调用 f2 的时候,那么会直接指向 C 一级的数据结构,因此不需要那些无用的检测。
cdef class A:
cdef public:
long a, b
def __init__(self, a, b):
self.a = a
self.b = b
cpdef long f2(self):
return self.a * self.b
# 规定接收一个 list,返回一个 long, 它们都是静态的,总之静态类型定义越多速度会越快
cpdef long func(list self_lst):
# 声明 long 类型的 s,A 类型的 self
# 我们下面使用的是 s = s + self.f2(), 所以这里的s要赋一个初始值0
cdef long s = 0
cdef A self
for self in self_lst:
s += self.f2()
return s
调用得到的结果是一样的,可以自己尝试一下。这样的话速度会变快很多,因为我们在循环的时候,规定了变量类型,并且求和也是一个只使用 C 的操作,因为 s 是一个 double。
这个版本的速度比之前快了 10 倍,这表明类型化比非类型化要快了 10 倍。如果我们删除了 cdef A self,也就是不规定其类型,而还是按照 Python 的语义来调用,那么速度仍然和之前一样,即便使用 cpdef 定义。所以重点在于指定类型为静态类型,只要规定好类型,那么就可以提升速度;而 Cython 是为 Python 服务的,肯定要经常使用 Python 的类型,那么提前规定好、让其指向 C 一级的数据结构,速度会提升很多。如果是 int 和 float,那么会自动采用 C 中的 int 和 float,当然怕溢出的话就使用 long、size_t、ssize_t、double,这样速度就更加快速了。因此重点是一定要静态定义类型,只要类型明确那么就能进行大量的优化。
Python 慢有很多原因,其中一个原因就是它无法对类型进行优化,以及对象分配在堆上。无法基于类型进行优化,就意味着每次都要进行大量的检测,当然这些我们前面已经说过了,如果规定好类型,那么就不用兜那么大圈子了;而对象分配在堆上这是无法避免的,只要你用 Python 的对象,都是分配在堆上,所以对于整型和浮点型,我们通过定义为 C 的类型使其分配在栈上,能够更加的提升速度。总之记住一句话:Cython 加速的关键就在于,类型的静态声明,以及对整数和浮点使用 C 中 long 和 double。
方法中给参数指定类型
无论是 def、cdef、cpdef,都可以给参数规定类型,如果类型传递的不对就会报错。比如:上面的 func 函数如果是普通的 Python 函数,那么内部的参数对于 Python 而言只要能够被 for 循环即可,所以它可以是列表、元组、集合。但是我们上面的 func 规定了类型,参数只能传递 list 对象或者其子类的实例对象,如果传递 tuple 对象就会报错。
然后我们来看看 __init__。
cdef class A:
cdef public:
long a, b
def __init__(self, float a, float b):
self.a = a
self.b = b
这里我们规定了类型,但是有没有发现什么问题呢?这里我们的参数 a 和 b 必须是一个 float,如果传递的是其它类型会报错,但是赋值的时候 self.a 和 self.b 又需要接收一个 long,所以这是一个自相矛盾的死结,在编译的时候就会报错。所以给 __init__ 参数传递的值的类型要和类中 cdef 声明的类型保持一致。
即使在类里面,cpdef 仍然不支持闭包。
然后为了更好地解释 Cython 带来的性能改进,我们需要了解关于继承、子类化、和扩展类型的多态性的基础知识。
类的继承与装饰
扩展类型只能继承单个基类,并且继承的基类必须是直接指向 C 实现的类型(可以是使用 cdef class 定义的扩展类型,也可以是内置类型,因为内置类型也是直接指向 C 一级的结构)。如果基类是常规的 Python 类(需要在运行时经过解释器动态解释才能指向 C 一级的结构),或者继承了多个基类,那么 Cython 在编译时会抛出异常。
cdef class Girl:
cdef public:
str name
long age
def __init__(self, name, age):
self.name = name
self.age = age
cpdef str get_info(self):
return f"name: {self.name}, age: {self.age}"
cdef class CGirl(Girl):
cdef public str where
def __init__(self, name, age, where):
self.where = where
super().__init__(name, age)
class PyGirl(Girl):
def __init__(self, name, age, where):
self.where = where
super().__init__(name, age)
我们定义了一个扩展类(Girl),然后让另一个扩展类(CGirl)和普通的 Python 类(PyGirl)都去继承它。我们说扩展类不可以继承 Python 类,但 Python 类是可以继承扩展类的。
import pyximport
pyximport.install(language_level=3)
import cython_test
c_girl = cython_test.CGirl("古明地觉", 17, "东方地灵殿")
print(c_girl.get_info()) # name: 古明地觉, age: 17
py_girl = cython_test.PyGirl("古明地觉", 17, "东方地灵殿")
print(py_girl .get_info()) # name: 古明地觉, age: 17
print(c_girl.where) # 东方地灵殿
print(py_girl.where) # 东方地灵殿
我们看到,对于扩展类和普通的 Python 类,它们都是可以继承扩展类的。
私有属性和私有方法
但是继承的话,子类是否可以访问父类的所有属性或方法呢?我们说cdef定义的方法和函数一样,无法被外部的Python访问,那么内部的 Python 类在继承的时候可不可以访问呢?以及私有属性(方法)的访问又是什么情况呢?
我们先来看看 Python 中关于私有属性的例子。
import pyximport
pyximport.install(language_level=3)
class A:
def __init__(self):
self.__name = "xxx"
def __foo(self):
return self.__name
try:
A().__name
except Exception as e:
print(e)
try:
A().__foo()
except Exception as e:
print(e)
"""
'A' object has no attribute '__name'
'A' object has no attribute '__foo'
"""
print(A()._A__name) # xxx
print(A()._A__foo()) # xxx
我们说定义的私有属性只能在当前类里面使用,一旦出去了就不能够再访问了。其实私有属性(方法)本质上只是 Python 给你改了个名字,在原来的名字前面加上一个 _类名
,所以 __name 和 __foo 其实相当于是 _A__name 和 _A__foo。但是当我们在外部用实例去获取 __name 和 __foo 的时候,获取的就是 __name 和 __foo,而显然 A 里面没有这两个属性或方法,因此报错。解决的办法就是使用 _A__name 和 _A__foo,但是不建议这么做,因为这是私有变量,如果非要访问的话,那就不要定义成私有的。如果是在 A 这个类里面获取的话,那么 Python 解释器也会自动为我们加上 _类名
这个前缀,比如我们在类里面获取 self.__name 的时候,实际上获取的也是 self._A__name,但是在外部就不会了。
_A__name = "古明地觉"
class A:
def __init__(self):
self.name = __name
# 是不是很神奇呢? 因为在类里面, __name 等价于 _A__name
print(A().name) # 古明地觉
如果是继承的话,会有什么结果呢?
class A:
def __init__(self):
self.__name = "xxx"
def __foo(self):
return self.__name
class B(A):
def test(self):
try:
self.__name
except Exception as e:
print(e)
try:
self.__foo()
except Exception as e:
print(e)
B().test()
"""
'B' object has no attribute '_B__name'
'B' object has no attribute '_B__foo'
"""
通过报错信息我们即可得知原因,B 也是一个类,那么在 B 里面调用私有属性,同样会加上 _类名
这个前缀,但是这个类名显然是 B 的类名,不是 A 的类名,因此找不到 _B__name 和 _B__foo,当然我们强制通过 _A__name 和 _A__foo 也是可以访问的,只是不建议这么做。
因此 Python 中不存在绝对的私有,只不过是解释器内部偷梁换柱将你的私有属性换了个名字罢了,但是我们可以认为它是私有的,因为按照原本的逻辑没有办法访问。同理继承的子类,也没有办法使用父类的私有属性。
但是在 Cython 中是不是这样子呢?
cdef class Person:
cdef public:
long __age
str __name
long length
def __init__(self, name, age, length):
self.__age = age
self.__name = name
self.length = length
cdef str __get_info(self):
return f"name: {self.__name}, age: {self.__age}, length: {self.length}"
cdef str get_info(self):
return f"name: {self.__name}, age: {self.__age}, length: {self.length}"
cdef class CGirl(Person):
cpdef test1(self):
return self.__name, self.__age, self.length
cpdef test2(self):
return self.__get_info()
cpdef test3(self):
return self.get_info()
静态类 CGirl 继承静态类 Person,那么 CGirl 对象能否使用 Person 里面的私有属性或方法呢?
import pyximport
pyximport.install(language_level=3)
import cython_test
c_g = cython_test.CGirl("古明地觉", 17, 156)
print(c_g.__name, c_g.__age, c_g.length) # 古明地觉 17 156
print(c_g.test1()) # ('古明地觉', 17, 156)
print(c_g.test2()) # name: 古明地觉, age: 17, length: 156
print(c_g.test3()) # name: 古明地觉, age: 17, length: 156
我们看到没有任何问题,对于静态类而言,子类可以使用父类中 cdef 定义的方法。除此之外,私有属性和私有方法也是可以使用的,就仿佛这些方法定义在自身内部一样。其实根本原因就在于对于静态类而言,里面的所有属性名称、方法名称都是所见即所得,比如我们设置了 self.__name,那么它的属性名就叫做 __name,不会在属性名的前面加上 "_类名",获取的时候也是一样。所以对于静态类而言,属性(方法)名称是否以双下划线开头根本无关紧要。
然后我们再来看看 Python 类继承静态类之后会有什么表现呢?
cdef class Person:
cdef public:
long __age
str __name
long length
def __init__(self, name, age, length):
self.__age = age
self.__name = name
self.length = length
cdef str __get_info(self):
return f"name: {self.__name}, age: {self.__age}, length: {self.length}"
cdef str get_info(self):
return f"name: {self.__name}, age: {self.__age}, length: {self.length}"
class PyGirl(Person):
def __init__(self, name, age, length, where):
self.__where = where
super(PyGirl, self).__init__(name, age, length)
def test1(self):
return self.__name, self.__age, self.length
def test2(self):
return self.__get_info()
def test3(self):
return self.get_info()
我们来测试一下:
import pyximport
pyximport.install(language_level=3)
import cython_test
py_g = cython_test.PyGirl("古明地觉", 17, 156, "东方地灵殿")
# 首先 __name、__age、length 都是在 Person 里面设置的,Person 是一个静态类
# 而我们说静态类里面没有那么多花里胡哨的,不会在以双下划线开头的成员变量前面加上 "_类名" 的
# 所以直接获取是没有问题的
print(py_g.__name) # 古明地觉
print(py_g.__age) # 17
print(py_g.length) # 156
# 但是 __where 不一样,它不是在静态类中设置的,所以它是会加上 "_类名" 的
try:
py_g.__where
except AttributeError as e:
print(e) # 'PyGirl' object has no attribute '__where'
print(py_g._PyGirl__where) # 东方地灵殿
try:
py_g.test1()
except AttributeError as e:
print(e) # 'PyGirl' object has no attribute '_PyGirl__name'
# 我们看到调用 test1 的时候报错了
# 原因就在于对于动态类而言,在类里面调用以双下划线开头的属性,会自动加上 "_类名",所以此时反而不正确了
try:
py_g.test2()
except AttributeError as e:
print(e) # 'PyGirl' object has no attribute '_PyGirl__get_info'
# 对于调用方法也是如此,因为解释器 "自作聪明" 的加上了 "_类名",导致方法名错了
# 但此刻我们还无法判断动态类实例对象是否能够调用静态类内部使用 cdef 定义的方法,因为方法名就不对
# 所以我们再执行一下 py_g.test3() 就能真相大白了
try:
py_g.test3()
except AttributeError as e:
print(e) # 'PyGirl' object has no attribute 'get_info'
因此结论很清晰了,静态类很单纯,里面的属性(方法)名称所见即所得,双下划线开头的属性(方法)对于静态类而言并没有什么特殊含义,动态类之所以不能调用是因为"多此一举"的在前面加上了 "_类名",导致方法名指定错了。然后是 cdef 定义的方法,即使是在 Cython 中,动态类也是不可以调用的,因为我们说 cdef 定义的是 C 一级的方法,它既不是 Python 的方法、也不像 cpdef 定义的时候自带 Python 包装器,因此它无法被子类继承,因此也就没有跨语言的边界。
如果将 cdef 改成 def 或者 cpdef,那么动态类就可以调用了。
在类里面使用 cpdef 定义的方法和在外部使用 cpdef 定义的函数一样,内部都不能出现闭包,换言之就是里面不能再定义函数。但是 cdef 和 def 是可以的,它们里面可以继续定义函数从而构建闭包。
但是注意:闭包对应的内层函数不可以是 cdef、cpdef 定义的函数,换言之只能是 def 定义的函数或者匿名函数。cdef、cpdef 在定义函数时只能出现在全局、或者类里面。
真正的私有
我们一直说双下划线开头的属性在静态类里面没有任何特殊的含义,因为是否私有不是通过名称前面是否有双下划线决定的,而是通过是否在类里面使用 cdef public 或者 cdef readonly 进行了声明所决定的,而且此时的私有是真正意义上的私有。如果不想让外界访问,那么外界是无论如何都访问不到的。
cdef class Person:
# 因为 __init__ 中有 self.where = ... 这行赋值语句,其表示要给 self 加一个名为 where 的属性
# 所以在类里面需要使用 cdef 进行声明,任何要绑定在 self 上面的属性都必须事先通过 cdef 声明好
# 并且声明的同时还可以指定类型,比如这里是 str,表示绑定在 self 上的 where 是 str 类型
# 那么 __init__ 中的参数 where 也必须要接收一个 str,否则 self.where = where 就是矛盾的
cdef public:
str where
# 此外这里在 cdef 后面还指定了 public,如果不指定的话,那么只能是 Cython 中创建的实例对象才可以访问
# 如果是在 Python 中导入这个类,那么实例化之后,是无法访问访问 where 属性的
# 也就是说,如果你不希望这里的 where 属性被外界的 Python 代码所访问,那么直接通过 cdef str where 进行声明即可
# 对于静态类而言,私有是通过这种方式实现的,而且此时的私有是真正的私有。
# 如果不想私有、想在 Python 中被访问的话,那么就是用 cdef public 声明
# 当然 cdef public 是外界既可以访问也可以修改,但如果只希望外界可以访问而不可以修改,那么就使用 cdef readonly
# 上面这些之前说过了,这里再重复一下
def __init__(self, where):
self.where = where
cdef class CGirl(Person):
# 这里也是同理,只不过是私有的
cdef:
str name
int age
int length
def __init__(self, name, age, length, where):
self.name = name
self.age = age
self.length = length
super(CGirl, self).__init__(where)
但是注意:对于 CGirl 而言,我们不需要声明 where,因为 self.where 的绑定是在 Person 中发生的,只要在 Person 中声明即可。由于 CGirl 继承 Person,如果 CGirl 中也声明了 where 那么返而报错,提示 where 重复声明了。
import pyximport
pyximport.install(language_level=3)
import cython_test
c_g = cython_test.CGirl("古明地觉", 16, 157, "东方地灵殿")
# where 是使用 cdef public 声明的,所以不是私有的
# name、age、length 是使用 cdef 声明的,所以是私有的
print(c_g.where) # 东方地灵殿
print(hasattr(c_g, "where")) # True
print(hasattr(c_g, "name")) # False
print(hasattr(c_g, "age")) # False
print(hasattr(c_g, "length")) # False
创建不可被继承的类
但是问题来了,如果我们希望自定义的扩展类不可以被其它类继承的话该怎么做呢?
cimport cython
# 通过 cython.final 进行装饰,那么这个类就不可被继承了
@cython.final
cdef class NotInheritable:
pass
通过 cython.final,那么被装饰的类就是一个不可继承类,不光是外界普通的 Python 类,内部的扩展类也是不可继承的。
import pyximport
pyximport.install(language_level=3)
import cython_test
class A(cython_test.NotInheritable):
pass
"""
TypeError: type 'cython_test.NotInheritable' is not an acceptable base type
"""
告诉我们 NotInheritable 不是一个合法的基类。
让扩展类实例可以被弱引用
我们知道 Python 的每一个对象都会有一个引用计数,当一个变量引用它时,引用计数会增加一。但我们可以对一个对象进行弱引用,弱引用的特点就是不会使对象的引用计数增加,举个栗子:
import sys
import weakref
class Girl:
def __init__(self):
self.name = "古明地觉"
g = Girl()
# 因为 g 作为 sys.getrefcount 的参数,所以引用计数会多 1
print(sys.getrefcount(g)) # 2
g2 = g
# 又有一个变量引用,所以引用计数增加 1,结果是 3
print(sys.getrefcount(g)) # 3
# 注意:这里是一个弱引用,不会增加引用计数
g3 = weakref.ref(g)
print(sys.getrefcount(g)) # 3
print(g3) # <weakref at 0x000001BFE84EF400; to 'Girl' at 0x000001BFCF9BA5B0>
# 删除 g、g2,对象的引用计数会变为 0,此时再打印 g3,会发现引用的对象已经被销毁了
del g, g2
print(g3) # <weakref at 0x00000222F0ED13B0; dead>
默认情况下,动态类实例对象都是可以被弱引用的,我们可以查看类的属性字典:
print(Girl.__dict__["__weakref__"]) # <attribute '__weakref__' of 'Girl' objects>
但是也有一个特例:
import weakref
class Girl:
__slots__ = ("name",)
def __init__(self):
self.name = "古明地觉"
# 一旦当定义了 __slots__ 的时候,这个类的实例对象就不可以被引用了
try:
weakref.ref(Girl())
except TypeError as e:
print(e) # cannot create weak reference to 'Girl' object
# 如果希望在定义 __slots__ 的时候也希望能被引用,那么就把 "__weakref__" 也加进去
class Girl:
__slots__ = ("name", "__weakref__")
def __init__(self):
self.name = "古明地觉"
# 此时就可以被弱引用了,只不过此时引用的对象也已经被销毁了,因为我们没有用变量指向它
print(weakref.ref(Girl())) # <weakref at 0x000002244C8CF450; dead>
那么问题来了,静态类或者说扩展类的实例对象可不可以被弱引用呢?我们拿内置类型来试试吧,因为我们说在 Cython 中定义的扩展类和内置的类是等价的,它们同属于静态类,如果内置类型的实例对象不可以被弱引用的话,那么 Cython 中定义的扩展类也是一样的结果。
import weakref
try:
weakref.ref(123)
except TypeError as e:
print(e) # cannot create weak reference to 'int' object
try:
weakref.ref("")
except TypeError as e:
print(e) # cannot create weak reference to 'str' object
try:
weakref.ref(())
except TypeError as e:
print(e) # cannot create weak reference to 'tuple' object
try:
weakref.ref({})
except TypeError as e:
print(e) # cannot create weak reference to 'dict' object
我们看到内置类型的实例是不可以被弱引用的,那么扩展类必然也是如此,其实也很好理解,因为要保证速度,自然会丧失一些 "花里胡哨" 的功能。但是问题来了,扩展类是我们自己实现的,我们就让其实例可以被弱引用该怎么办呢?
cdef class A:
# 类似于动态类中的 __slots__,只需要声明一个 __weakref__ 即可
cdef object __weakref__
cdef class B:
pass
import weakref
import pyximport
pyximport.install(language_level=3)
import cython_test
# A 实例是可以被引用的,因为我们指定了 __weakref__
print(weakref.ref(cython_test.A())) # <weakref at 0x0000016E962D2220; dead>
# 但是 B 实例默认则是不允许的,因为它是没有指定 __weakref__ 的静态类
try:
print(weakref.ref(cython_test.B()))
except TypeError as e:
print(e)
以上就是让扩展类实例支持弱引用的方式。
扩展类实例对象的销毁以及垃圾回收
我们说当对象的引用计数为 0 时,会被销毁,这个销毁可以是放入缓存池中、也可以是交还给系统堆,当然不管哪一种,我们都不能再用了。
x = "古明地觉"
x = "古明地恋"
在执行完第二行的时候,由于 x 指向了别的字符串,因此第一个字符串对象引用计数为 0、会被销毁。而这个过程在底层会调用 tp_dealloc,因为 Python 的类对象在底层对应的都是一个 PyTypeObject 结构体实例,其内部有一个 tp_dealloc 成员是专门负责其实例对象的销毁的。
因此判断一个 Python 中的对象是否会被销毁非常简单,就看它的引用计数,只要引用计数为 0,就会被销毁,不为 0,就不会被销毁,就这么简单。但是引用计数最大的硬伤就是它解决不了循环引用,所以 Python 才会有垃圾回收机制,专门负责解决循环引用。
class Object:
pass
def make_cycle_ref():
x = Object()
y = [x]
x.attr = y
当我们调用 make_cycle_ref 函数时,就会出现循环引用,x 内部引用了 y 指向的对象、y 内部又引用了 x 指向的对象,如果我们将垃圾回收机制关闭的话,即使函数退出,对象也不会被回收。而如果想解决这一点,那么就必须在销毁对象之前先将对象内部引用的其它对象的引用计数减一才行,也就是打破循环引用,这便是 Python 底层的垃圾回收器所做的事情。
所以上面在销毁 x 时(x 是一个变量,这里为了描述方便,就用 x 代指其指向的对象,后面的 y 同理),先将内部的属性 attr 引用的 y 的引用计数减 1,然后再将 x 的引用计数减 1,但是此时 x 还没有销毁,因为它的引用计数还是 1;然后销毁 y,在销毁 y 之前,先将内部引用的 x 的引用计数减 1,此时 x 的引用计数就变成 0 了。接着再将 y 的引用计数减 1,由于在销毁 x 的时候,已经将 y 的引用计数减 1 了,所以此时 y 的引用计数也为 0。所以最终 x 和 y 的引用计数都会变成 0,最终都会被销毁,所以垃圾回收只是将发生循环引用的对象的引用计数减去 1,而对象是否被销毁还是由引用计数是否为 0 所决定的。
而如果想做到这一点,那么就必须在 tp_traverse 中指定垃圾回收器要跟踪的属性,PyTypeObject 内部有一个 tp_traverse 成员,它接收一个函数,在内部指定要跟踪的属性(x 的话就是 attr,y 由于是一个列表,它里面的每一个元素都要跟踪)。垃圾回收器根据 tp_traverse 指定的要跟踪的属性,找到这些属性引用的其它对象;然后 PyTypeObject 内部还有一个 tp_clear,在这里面再将引用的其它对象的引用计数减 1,所以寻找(tp_traverse)和清除(tp_clear)是在两个函数中实现的。
tp_traverse:指定垃圾回收器要跟踪的属性,垃圾回收器会找到这些属性引用的对象
tp_clear:将 tp_traverse 中找到的属性引用的对象的引用计数减 1
tp_dealloc:负责对象本身被销毁时的工作,在扩展类中可以用 __dealloc__ 实现
禁用 tp_clear
对于扩展类而言,默认是支持垃圾回收的,底层会自动生成 tp_traverse 和 tp_clear,显然这也是我们期待的结果。但在某些场景下,就不一定是我们期待的了,比如你需要在 __dealloc__ 中清理某些外部资源,但是你的对象又恰好在循环引用当中,举个栗子:
cdef class DBCursor:
cdef DBConnection conn
cdef DBAPI_Cursor *raw_cursor
# ...
def __dealloc__(self):
DBAPI_close_cursor(self.conn.raw_conn, self.raw_cursor)
当我们在销毁对象时,想要通过数据库连接来关闭游标,但如果游标碰巧处于循环引用当中,那么垃圾回收器可能会删除数据库连接,从而无法对游标进行清理。所以解决办法就是禁用该扩展类的 tp_clear,通过 no_gc_clear 装饰器来实现:
cimport cython
@cython.no_gc_clear
cdef class DBCursor:
cdef DBConnection conn
cdef DBAPI_Cursor *raw_cursor
# ...
def __dealloc__(self):
DBAPI_close_cursor(self.conn.raw_conn, self.raw_cursor)
如果使用 no_gc_clear,那么引用循环中至少有一个没有 no_gc_clear 的对象,否则循环引用无法被打破,从而引发内存泄露。
禁用垃圾回收
我们说垃圾回收是为了解决循环引用而存在的,解释器会将那些能产生循环引用的对象放在链表(零代、一代、二代)上,然后从根节点出发进行遍历,而显然链表上的对象越少,垃圾回收的耗时就越短。所以如果一个对象可以发生循环引用,但是我们能保证实际情况中它不会发生,那么我们就可以让这个对象不参与垃圾回收,从而提升效率。
比如我们可以定义一个不可能发生循环引用的扩展类:
cdef class Girl:
cdef public str name
cdef public int age
我们知道扩展类的实例对象是可以发生循环引用的,所以它默认会被挂到链表上,但是很明显,对于我们当前这个扩展类而言,它的实例对象是不会发生循环引用的。因为内部只有两个属性,分别是字符串和整型,加上扩展类无法动态添加属性,所以实际情况中 Girl 的实例不可能产生循环引用。但是解释器不会做这种假设,所以依旧会将其挂到链表上,因此我们可以使用 no_gc 装饰器来阻止解释器这么做。
cimport cython
@cython.no_gc
cdef class Girl:
cdef public str name
cdef public int age
此时程序的运行效率会得到提升,当然要注意:使用 no_gc 一定要确保不会发生循环引用,如果给上面的类再添加一个声明:
cimport cython
@cython.no_gc
cdef class Girl:
cdef public str name
cdef public int age
cdef public list hobby
这个时候就必须要小心了,因为里面出现了列表,列表是可以发生循环引用的。
启动 trashcan
在 Python 中,我们可以创建具有深度递归的对象,比如:
L = None
for i in range(2 ** 20):
L = [L]
del L
当我们删除 L 的时候,会先销毁打印 L[0]
、然后销毁 L[0][0]
,以此类推,直到递归深度为 2 ** 20。而这样的深度很容易溢出 C 的调用栈,导致 Python 解释器崩溃,但事实上我们上面在 del L
的时候解释器并没有崩溃,原因就是 CPython 发明了一种名为 trashcan 的机制,它通过延迟销毁的方式来限制销毁的递归深度。比如可以查看 CPython 源代码 Object/listobject.c 的 list_dealloc 函数,在销毁列表时有体现:
static void
list_dealloc(PyListObject *op)
{
Py_ssize_t i;
PyObject_GC_UnTrack(op);
Py_TRASHCAN_BEGIN(op, list_dealloc) // 限制销毁的递归深度
/*
...
...
...
*/
Py_TRASHCAN_END
}
但是对于扩展类而言,则并非如此,它默认是没有开启 transcan 机制的:
cdef class A:
def __init__(self):
cdef list L = None
cdef unsigned long i
for i from 0 <= i < 2 ** 30:
L = [L]
如果你导入 A 这个类并实例化,那么你的内存占用率会越来越高,最终程序崩溃。如果希望扩展类实例对象也能开启 transcan 机制,同样可以使用装饰器:
cimport cython
@cython.transcan(True)
cdef class A:
def __init__(self):
cdef list L = None
cdef unsigned long i
for i from 0 <= i < 2 ** 30:
L = [L]
如果一个类开启了 transcan 机制,那么继承它的子类也会开启,如果不想开启,则需要通过 transcan(False)
显式关闭。
说实话以上这些其实不是特别常用,一旦使用就意味着你要格外小心,因为涉及内存都是有危险的,所以如果无法百分百确认自己是否需要这么做,那就不要做。而且像 transcan 这种,个人觉得真正写项目的时候很少会出现,了解一下即可。
扩展类实例的 freelist
有些时候我们需要多次对某个类执行实例化和销毁操作,这也意味着会有多次内存的创建与销毁。那么我们能不能像Python 底层采用的缓存池策略一样,每次销毁的时候不释放内存,而是放入一个链表(freelist)中,当申请的时候直接从链表中获取即可。
cimport cython
# 声明一个可以容纳 8 个实例的链表,每当销毁的时候就会放入到链表中,最多可以放 8 个
# 如果销毁第 9 个实例,那么就不会再放到 freelist 里了
@cython.freelist(8)
cdef class Girl:
cdef str name
cdef int age
def __init__(self, name, age):
self.name = name
self.age = age
girl = Girl("古明地觉", 16)
del girl # 会放入到 freelist 中,里面的元素个数加 1
girl = Girl("雾雨魔理沙", 17) # 从 freelist 中获取,里面元素个数减 1,此时无需重新申请内存
扩展类实例的序列化和反序列化
然后说一下序列化和反序列化,像内置 pickle、json 库都可以将对象序列化和反序列化,这里我们说的是 pickle。pickle 和 json 不同,json 序列化之后是人类可阅读的,但是能序列化的对象是有限的,因为序列化的结果可以在不同语言之间传递;但是 pickle 序列化之后是二进制格式,只有 Python 才认识,因此它可以序列化 Python 的绝大部分对象。
import pickle
class Girl:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
girl = Girl("古明地觉", 16)
# 这便是序列化话的结果
dumps_obj = pickle.dumps(girl)
# 显然这是什么东西我们不认识,但是 Python 解释器认识,我们可以再进行反序列化
print(dumps_obj[: 20]) # b'\x80\x04\x95;\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main_'
loads_obj = pickle.loads(dumps_obj)
print(loads_obj.name) # 古明地觉
print(loads_obj.age) # 16
这里我们不探究 pickle 的实现原理,我们来说一下如何自定制序列化和返序列化的过程,想要自定制的话,需要实现 __getstate__ 和 __setstate__ 两个魔法方法:
import pickle
class Girl:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __getstate__(self):
"""序列化的时候会调用"""
# 对 Girl 的实例对象进行序列化的时候,默认会返回其属性字典
# 这里我们多添加一个属性
print("被序列化了")
return {**self.__dict__, "gender": "女"}
def __setstate__(self, state):
"""反序列化时会调用"""
# 对 Girl 的实例对象进行序列化的时候,会将 __getstate__ 返回的字典传递给这里的 state 参数
# 我们再设置到 self 当中,如果不设置,那么序列化之后是无法获取属性的
print("被反序列化了")
self.__dict__.update(**state)
girl = Girl("古明地觉", 16)
dumps_obj = pickle.dumps(girl)
"""
被序列化了
"""
loads_obj = pickle.loads(dumps_obj)
print(loads_obj.name)
print(loads_obj.age)
print(loads_obj.gender)
"""
被反序列化了
古明地觉
16
女
"""
推荐阅读
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,
-
Cython 系列 4.定义和使用 Cython 中的扩展类,分析它们与 Python 中普通类的区别。
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面