楔子
关于 Cython,我们目前已经详细介绍了它的语法以及使用方式,不过版本是 0.29。而前一段时间,Cython 发布了 3.0 版本,那么它和 0.29 版本相比都发生了哪些变化呢?
本次就来聊一聊其中的一些比较大的变化。
变量支持非 ASCII 字符
在 0.29 以及之前的版本中,Cython 要求变量名必须是 ASCII 字符,但在 3.0 版本中,这一限制被取消了。
先来看看 0.29 版本。
# 文件名:cython_test.pyx cpdef str 你好世界(): return "Hello World"
如果我们尝试编译这个文件,那么会报错。
告诉我们标识符不合法,但如果使用 Cython3.0 编译的话,那么结果没有任何问题。
使用 3.0 版本。
import pyximport pyximport.install(language_level=3) import cython_test print(cython_test.你好世界()) # Hello World
可以看到使用 3.0 版本的 Cython 没有任何问题,所以 0.29 和 3.0 之间的一个区别就是 3.0 支持使用非 ASCII 字符(比如中文)定义变量。
但说实话,这个功能没太大用,因为在开发中也基本不会用中文给变量命名。
开启 generator_stop
对于生成器而言,return 的本质就是向外抛出一个 StopIteration 异常,代表这个生成器结束了。
def gen(): yield 1 yield 2 yield 3 return "结束啦" g = gen() print(g.__next__()) # 1 print(g.__next__()) # 2 print(g.__next__()) # 3 try: g.__next__() except StopIteration as e: print(e.value) # 结束啦
但在 Python 3.7 版本之前,我们也可以手动引发一个 StopIteration。
def gen1(): yield 1 yield 2 yield 3 return "结束啦" def gen2(): yield 1 yield 2 yield 3 raise StopIteration("结束啦")
在 3.7 以前,上面的两个生成器函数是等价的,但这就产生了一个问题。举个例子:
def gen(): yield 1 yield 2 # 只是单纯地抛出一个 StopIteration # 但在 Python 3.7 之前,它等价于 return "middle value" raise StopIteration("middle value") yield 3 return "结束啦"
所以为了消除这样的误会,从 Python 3.7 开始,结束生成器一律通过 return。至于生成器内部引发的 StopIteration 则会被转化为 RuntimeError。
而在 Python 3.7 之前如果想开启这一功能,需要通过 __future__ 来实现。
from __future__ import generator_stop
然后重点来了,如果你使用的是 Cython 3.0,并且以 py3 的模式编译,那么即使解释器版本低于 3.7,这一功能默认也是开启的。
说实话,这个功能对我们来说,也没有什么用。
默认以 py3 的模式编译
在 Cython 0.29 的时候,默认是以 Python2 的语义编译 .pyx 的,如果希望以 Python3 的语义进行编译,那么需要将 language_level 参数指定为 3,否则就会抛出警告。
但从 Cython 3.0 开始,默认则是以 Python3 的语义进行编译,如果希望兼容 Python2,那么需要显式地将该参数指定为 2。
除了极端情况,我们完全不需要兼容 Python2,因为会失去 Python3 的很多优秀特性。
__init_subclass__ 的问题
之前介绍过 __init_subclass__ 这个魔法方法,它在一些简单的场景下可以替代元类,举个例子。
class Base: def __init_subclass__(cls, **kwargs): """ 钩子函数,当该类被继承时会自动触发此函数 注意:cls 不是当前的 Base,而是继承 Base 的类 """ for attr, val in kwargs.items(): type.__setattr__(cls, attr, val) class Girl(Base, name="古明地觉", address="地灵殿"): pass print(Girl.name) # 古明地觉 print(Girl.address) # 地灵殿
需要注意的是,__init_subclass__ 这个函数是被 classmethod 隐式装饰的,当然我们也可以显式地装饰它。
class Base: @classmethod def __init_subclass__(cls, **kwargs): for attr, val in kwargs.items(): type.__setattr__(cls, attr, val)
在 Python 代码中,上面两种做法都是可以的。但如果是在 Cython 里面,则必须要显式装饰,否则就会出现参数错误,因为 Cython 不会帮你隐式装饰。
下面举例说明:
# 文件名:cython_test.pyx class Base: def __init_subclass__(cls, **kwargs): for attr, val in kwargs.items(): type.__setattr__(cls, attr, val)
我们将 Base 类的定义移动到了 pyx 文件中,然后来导入它。
import pyximport pyximport.install(language_level=3) from cython_test import Base try: class Girl(Base): pass except TypeError as e: print(e) """ __init_subclass__() takes exactly 1 positional argument (0 given) """
告诉我们 __init_subclass__ 需要一个位置参数,如果想解决这一点,那么使用 classmethod 显式装饰一下即可。
但以上都是 Cython 0.29 版本以及之前才会出现的问题,如果是 Cython 3.0,那么表现和纯 Python 代码是一样的,也会隐式装饰。
类型注解延迟解析
先说一下类型注解,Python 从 3.5 开始支持类型注解。
class A: pass def foo(a: A, b: str, c: int): pass print(foo.__annotations__) """ {'a': <class '__main__.A'>, 'b': <class 'str'>, 'c': <class 'int'>} """
像 FastAPI、Pydantic 等框架都高度依赖 Python 的类型注解功能,然后需要注意的是,类型注解在定义函数的时候就被解析了,所以下面这种做法就会出问题:
class A: @classmethod def create_instance(cls) -> A: pass """ NameError: name 'A' is not defined """
定义类 A,它内部有一个 create_instance 类方法,通过类型注解表示该方法会返回一个 A 的实例对象。但这个类在定义的时候却报错了,原因就是 Python 在解析的时候,A 这个类还没有来得及创建。
所以从 Python3.7 开始,便又引入了类型注解延迟解析:
# 3.7 开始支持类型注解延迟解析,但必须导入 annotations # 而 3.10 开始则不再需要,会变成默认行为 from __future__ import annotations class A: # 解释器在解析 foo 的时候,B 还没有定义 # 不过没有关系,因为类型注解会被延迟解析 def foo(self, b: B): pass class B: pass # 启用延迟类型注解后,Python 会把类型提示存储为字符串 # 所以 value 不再是 <class '__main__.B'>,而是字符串 "B" print(A.foo.__annotations__) # {'b': 'B'} # 当调用 typing.get_type_hints() 时才进行解析 import typing print(typing.get_type_hints(A.foo)) # {'b': <class '__main__.B'>}
如果你不想使用 __future__ 的话,那么也可以换一种方式。
class A: def foo(self, b: "B"): pass class B: pass print(A.foo.__annotations__) # {'b': 'B'} import typing print(typing.get_type_hints(A.foo)) # {'b': <class '__main__.B'>}
在声明的时候直接指定为字符串即可,这样即便 Python 版本低于 3.10,也是可以的。
然后类型注解在 Cython 中也是支持的,只不过在 Cython 中我们更习惯使用 C 风格定义变量。
# 在定义 C 级变量的时候,必须使用 C 风格进行变量声明 cdef str name = "古明地恋" # 注意:不可以写成 cdef name: str = "古明地恋",这是错误的语法 # 但在函数中是可以的 # `类型 变量` 属于 C 风格,比如 list data # `变量: 类型` 属于 Python 风格,比如 target: int cpdef Py_ssize_t search(list data, target: int): if target in data: return data.index(target) return -1
然后是返回值的问题,如果使用 cdef、cpdef 定义函数,那么返回值类型声明必须写在 cdef、cpdef 后面。
cpdef list foo(): return [1, 2, 3, 4, 5] # 但 cpdef foo() -> list: 这么做是不合法的,会编译错误 """ Return type annotation is not allowed in cdef/cpdef signatures """ # 事实上 cdef 和 cpdef 后面如果不指定类型,那么默认是 object # 这种做法只能用在 def 定义的函数中 def bar() -> tuple: return 1, 2, 3
最后在 Cython 中同样也支持延迟类型注解。
# 文件名: cython_test.pyx class A: def foo(self, b: "B"): pass class B: pass
如果是 Cython 0.29,那么必须写成 "B",否则会出现 NameError。但从 Cython 3.0 开始,即使不写成字符串的形式,也没有任何问题。
但说实话,在 Cython 中声明变量,最好使用 C 风格的方式。也就是类型 变量的方式,而不要使用变量: 类型。
仅限位置参数
在 Python 中,可以强制要求某些参数只能通过关键字参数或位置参数的方式进行传递。
# 这里的 * 表示参数 c 和 d 必须通过关键字参数的方式传递 # 所以即便非默认参数 d 在默认参数 c 的后面也没有关系 def foo(a, b, *, c=123, d): pass # / 要求它前面的参数(这里是 a 和 b) 必须通过位置参数的方式传递 def bar(a, b, /, c, d): pass
但 Cython 在 0.29 的时候,只支持仅限关键字参数(*),不支持仅限位置参数(/)。而 Cython 在 3.0 的时候,这两者则都支持。
赋值表达式
这是 Python3.8 新增的一个功能,可以在表达式当中完成赋值。
import re date = "2020-03-04" match = re.search(r"(\d{4})-(\d{2})-(\d{2})", date) if match is not None: year, month, day = match.groups() print(year, month, day) # 2020 03 04 # 通过赋值表达式,可以将赋值和比较一步完成 if (match := re.search(r"(\d{4})-(\d{2})-(\d{2})", date)) is not None: year, month, day = match.groups() print(year, month, day) # 2020 03 04
赋值表达式是我个人比较喜欢的一个功能,但 Cython 在 0.29 以及之前是不支持的,从 3.0 开始才支持。
接下篇:https://developer.aliyun.com/article/1617409