分享

Cython 的融合类型

 古明地觉O_o 2022-12-08 发布于北京

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(<short1, <short2))
print(integral_max(<int1, <int2))
print(integral_max(<long1, <long2))

如果一个融合类型声明了多个参数,那么这些参数的类型都必须是融合类型中的同一种,所以下面的调用都是不合法的。

print(integral_max(<short1, <int2))
print(integral_max(<int1, <long2))
print(integral_max(<long1, <short2))

融合类型相当于多个类型的组合,比如 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([12], [34])
)  # [1, 2, 3, 4]

# 我们也可以显式指定要调用的函数版本
print(
    func[
list]([1122], [3344])
)  # [11, 22, 33, 44]

print(
    func[
tuple]((111222), (333444))
)  # (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[listdict]([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[
listdict]([1], {"x"""}, 123, [], {}) 
func[
listset]([1], {123}, 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)(listsetintlistset)
# 赋值
func_with_list_set = func
func([], {1}, 123, [], {2})
"""
[] {1} [] {2} 123
"""


# 或者这种方式也是可以的
# 将 func 转成 <object (*)(list, set, int, list, set)>
# 相当于将函数指针转成了接收五个参数、返回一个object类型的指针
(<object (*)(listsetintlistset)> func)([], {1}, 123, [], {2})
"""
[] {1} [] {2} 123
"""


# 还有就是之前的方式,只不过可以拆开使用
# [] 里面只需要指定融合类型
cdef func_with_tuple_dict = func[tuple, dict]
func_with_tuple_dict((12), {"a""b"}, 456, (1122), {"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 中的泛型编程还是很强大的,但是在工作中的使用频率其实并不是那么频繁。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多