Cython 将静态类型系统引入到了 Python 中,实现了基于类型的优化。但问题来了, 如果一个变量可能是不同的类型,该怎么办呢?比如一个变量既可能是整型,也可能是浮点型。
而 Cython 也考虑到了这一点,就是下面要介绍的融合类型。说融合类型可能让人感到陌生,如果说泛型是不是就很熟悉了。
Cython 目前提供了三种我们可以直接使用的融合类型,integral、floating、numeric,含义如下:
- integral:等价于 C 的 short, int, long;
- floating:等价于 C 的 float, double;
- numeric:最通用的类型,包含上面的 integral、floating 以及复数;
当然这三个融合类型无法直接用,需要通过 cimport 导入。
from cython cimport integral cpdef integral integral_max(integral a, integral b): return a if a >= b else b
上面这段代码,Cython 将会创建三个版本的函数:1)参数 a 和 b 都是 short 类型;2)参数 a 和 b 都是 int 类型;3)参数 a 和 b 都是 long 类型。
在调用的时候可以显式指定类型,否则会选择范围最大的类型,举个例子:
print(integral_max(<short> 1, <short> 2)) print(integral_max(<int> 1, <int> 2)) print(integral_max(<long> 1, <long> 2))
如果一个融合类型声明了多个参数,那么这些参数的类型都必须是融合类型中的同一种,所以下面的调用都是不合法的。
print(integral_max(<short> 1, <int> 2)) print(integral_max(<int> 1, <long> 2)) print(integral_max(<long> 1, <short> 2))
融合类型相当于多个类型的组合,比如 integral 是 short, int, long 的组合,至于 integral 最终会表现出哪一种,则取决于传递的参数。但融合类型在同一时刻,只能表示一种类型,什么意思呢?比如我们上面的参数 a 和 b 的类型是相同的,都是 integral 类型,那么最终 a 和 b 要么都是 short、要么都是 int、要么都是 long,不存在 a 是 int、b 是 short 这种情况。
当然这背后的原理我们也说了,如果出现了融合类型,那么 Cython 会根据融合类型里面的每一个类型都单独创建一个函数。在调用时,根据传递参数类型,来判断调用哪一个版本的函数。
到目前为止,总共出现了三个融合类型,都需要从 cython 这个名字空间里面 cimport 之后才能使用。那么问题来了,我们能不能自己创建融合类型呢?答案是可以的。
ctypedef fused list_tuple: list tuple # a 和 b 要么都为列表、要么都为元组 # 但不可以一个是列表、一个是元组 cpdef list_tuple func(list_tuple a, list_tuple b): return a + b # Cython 会根据我们传递的参数来判断,调用哪一种函数 print( func([1, 2], [3, 4]) ) # [1, 2, 3, 4] # 我们也可以显式指定要调用的函数版本 print( func[list]([11, 22], [33, 44]) ) # [11, 22, 33, 44] print( func[tuple]((111, 222), (333, 444)) ) # (111, 222, 333, 444)
还是挺简单的,并且组合成融合类型的多个类型,可以是 C 的类型,也可是 Python 的类型。
另外再次强调,list_tuple 虽然既可以是 list,也可以是 tuple,但是在同一个函数中只能表现出一种类型。如果我们给 a 传递 list、给 b 传递 tuple,看看会有什么结果。
import pyximport pyximport.install(language_level=3) import cython_test try: cython_test.func([], ()) except TypeError as e: print(e) """ Argument 'b' has incorrect type (expected list, got tuple) """ # 当 a 接收的是一个列表时 # 那么就可以将 list_tuple 看成是 list 了 # 于是 b 也必须接收一个列表 try: cython_test.func((), []) except TypeError as e: print(e) """ Argument 'b' has incorrect type (expected tuple, got list) """ # 当 a 接收的是一个元组时 # 那么就可以将 list_tuple 看成是 tuple 了 # 于是 b 也必须接收一个元组
另外我们上面只出现了一种融合类型,我们还可以定义多种。
ctypedef fused list_tuple: list tuple ctypedef fused dict_set: dict set # 会生成如下四种版本的函数: # 1) 参数 a、c 为列表,b、d 为字典 # 2) 参数 a、c 为列表,b、d 为集合 # 3) 参数 a、c 为元组,b、d 为字典 # 4) 参数 a、c 为元组,b、d 为集合 cdef func(list_tuple a, dict_set b, list_tuple c, dict_set d): print(a, b, c, d) # 会根据我们传递参数来判断选择哪一个版本的函数 func([1], {"x": ""}, [], {}) """ [1] {'x': ''} [] {} """ # 依旧可以显式指定类型,不让 Cython 帮我们判断 # 但由于存在多种混合类型 # 一旦指定、那么每一个混合类型都要指定 func[list, dict]([1], {"x": ""}, [], {}) """ [1] {'x': ''} [] {} """
此外,我们必须写成 func[list, dict] 这种形式,不可以是 func[dict, list]。因为类型为 list_tuple 的参数先出现,类型为 dict_set 的参数后出现。所以中括号里面第一个出现的类型一定是 list_tuple 里面的类型(list 或 tuple),第二个是 dict_set 里面的类型(dict 或 set)。
因此一旦指定版本,那么只能是以下四种之一:
- func[list, dict](...)
- func[list, set](...)
- func[tuple, dict](...)
- func[tuple, set](...)
当然啦,别忘记在传参的时候务必保证参数类型正确。
多说一句题外话,如果你用过 Go 的话,你会发现 Go 的泛型和 Cython 的融合类型非常相似,我们举个栗子。
Go 泛型:
Cython 融合类型:
对比一下之后,是不是发现两者非常像呢?但很明显,Cython 的融合类型、或者也叫泛型,在设计上要更优秀一些。比如定义完 T 之后,直接使用 T 即可;而 Go 里面在定义完 T 之后还不能直接用,必须要再起一个名字(T1),然后用这个新起的名字。
好了,言归正传,在定义函数时,不仅仅只有融合类型,还可以有具体的类型,举个例子:
ctypedef fused list_tuple: list tuple ctypedef fused dict_set: dict set # 里面除了融合类型之外,还有一个 int 类型 cdef func(list_tuple a, dict_set b, int xxx, list_tuple c, dict_set d): print(a, b, c, d, xxx) # 显然调用是无影响的,因为在 func 后面的 [ ] 里面 # 只需要指定融合类型对应的具体类型,其它的不需要管 func[list, dict]([1], {"x": ""}, 123, [], {}) func[list, set]([1], {1, 2, 3}, 456, [], {2})
最后,上面的 func 函数还有一种调用方式,我们来看一下:
cdef func(list_tuple a, dict_set b, int xxx, list_tuple c, dict_set d): print(a, b, c, d, xxx) # 声明一个函数指针,指向的函数接收五个参数 # 类型分别是 list, set, int, list, set,返回 object # 此时必须将所有参数的类型全部指定,不能只指定融合类型 # 并且声明为同一种融合类型的参数的具体类型仍然要一致 cdef object (*func_with_list_set)(list, set, int, list, set) # 赋值 func_with_list_set = func func([], {1}, 123, [], {2}) """ [] {1} [] {2} 123 """ # 或者这种方式也是可以的 # 将 func 转成 <object (*)(list, set, int, list, set)> # 相当于将函数指针转成了接收五个参数、返回一个object类型的指针 (<object (*)(list, set, int, list, set)> func)([], {1}, 123, [], {2}) """ [] {1} [] {2} 123 """ # 还有就是之前的方式,只不过可以拆开使用 # [] 里面只需要指定融合类型 cdef func_with_tuple_dict = func[tuple, dict] func_with_tuple_dict((1, 2), {"a": "b"}, 456, (11, 22), {"b": "a"}) """ (1, 2) {'a': 'b'} (11, 22) {'b': 'a'} 456 """
到此,关于融合类型的创建和用法我们就说完了,总之融合类型不仅可以用在函数的参数和返回值中,也可以用于普通的变量声明。
但是变量到底是融合类型的哪一种,还需要我们动态判断。
ctypedef fused list_tuple_dict: list tuple dict # 在判断的时候,可以对 val 进行判断 # 比如使用 type 或者 isinstance # 但是我们还可以对融合类型本身判断 cpdef func(list_tuple_dict val): """ Cython 会根据该函数生成以下三个函数 cdef func(list val) cdef func(tuple val) cdef func(dict val) 根据 val 类型的不同,调用不同版本的函数 所以不管最终调用的是哪一个版本的函数 类型都是确定的 """ # 因此在编写代码的时候 # 根据融合类型本身就可以判断 if list_tuple_dict is list: print("val 是 list 类型") elif list_tuple_dict is tuple: print("val 是 tuple 类型") else: print("val 是 dict 类型")
然后我们调用一下试试:
import pyximport pyximport.install(language_level=3) import cython_test cython_test.func([]) cython_test.func(()) cython_test.func({}) """ val 是 list 类型 val 是 tuple 类型 val 是 dict 类型 """ # 如果类型不是融合类型中的任意一种 # 那么就会报错 try: cython_test.func(123) except TypeError as e: print(e) """ No matching signature found """
混合类型具体会是哪一种类型,在参数传递的时候便会得到确定。
因此 Cython 中的泛型编程还是很强大的,但是在工作中的使用频率其实并不是那么频繁。