花咲く猫言語: Cython は、Python と C 言語の機能を組み合わせたプログラミング言語です。Python 拡張モジュールの作成によく使用され、Python のパフォーマンスを向上させる一般的な手段です。Gu Mingdijue氏が連載している「初心者からマスターまでCython」シリーズがオススメです!ここで共有される記事では、Cython の最新バージョンの関連機能が紹介されています。
出典: Gu Ming Dijue のプログラミング教室
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
Cython の 3.0 バージョンを使用しても問題がないことがわかります。0.29 と 3.0 の違いの 1 つは、3.0 では変数の定義に非 ASCII 文字 (中国語など) の使用をサポートしていることです。
しかし、正直なところ、開発中に中国語で変数名を付けることは基本的にできないので、この機能はあまり役に立ちません。
ジェネレータ_ストップを有効にする
ジェネレーターの場合、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 より前では、上記の 2 つのジェネレーター関数は同等でしたが、これにより問題が発生しました。例えば:
def gen():
yield 1
yield 2
# 只是单纯地抛出一个 StopIteration
# 但在 Python 3.7 之前,它等价于 return "middle value"
raise StopIteration("middle value")
yield 3
return "结束啦"
そのため、このような誤解を避けるために、Python 3.7 以降では、end ジェネレーターは常に return を渡すようになりました。ジェネレーター内で発生した StopIteration については、RuntimeError に変換されます。
Python 3.7 より前では、この機能を有効にしたい場合は、__future__ を通じて実装する必要があります。
from __future__ import generator_stop
次に、重要な点が来ます。Cython 3.0 を使用し、py3 モードでコンパイルしている場合、インタープリターのバージョンが 3.7 より低い場合でも、この機能はデフォルトで有効になります。
正直に言うと、この機能は私たちには役に立ちません。
デフォルトで py3 モードでコンパイルします
Cython 0.29 では、.pyx はデフォルトで Python2 セマンティクスでコンパイルされます。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__ はクラスメソッドによって暗黙的に修飾されることに注意してください。もちろん、明示的に修飾することもできます。
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__ には位置パラメータが必要であることを伝えてください。これを解決したい場合は、クラスメソッドを使用して明示的に装飾します。
ただし、上記はすべて 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
"""
内部に create_instance クラス メソッドを持つクラス A を定義します。型アノテーションは、このメソッドが 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 スタイルの方法で行うのが最適です。つまり、variable: type を使用する代わりに、変数を型指定する方法です。
位置パラメータのみ
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 以降でのみサポートされています。
デコレータの構文制限を緩和する
Python 3.9 より前は、次の方法でデコレータを使用することはできませんでした。
from functools import wraps
class Button:
def __init__(self, n):
self.n = n
def deco(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
buttons = [Button(i) for i in range(1, 10)]
@buttons[0].deco
def foo():
pass
そうすると、構文エラーが発生します。
これを行う必要があります。
@eval("buttons[0].deco")
def foo():
pass
# 或者
button = buttons[0]
@button.deco
def bar():
pass
@buttons[0].deco このメソッドは Python3.9 からのみサポートされています。同様に、Cython バージョン 0.29 は .pyx でのこの構文の使用をサポートしていませんが、Cython 3.0 からサポートされています。
Cython 3.0 を使用する場合は、3.9 より前の Python バージョンでも問題ありません。pyx ファイルの構文解析は Cython コンパイラによって行われるため、Python インタプリタとは何の関係もありません。
例外の伝播
Cythonの例外処理を紹介する際に、関数の戻り値の型がC型の場合、関数内の例外は無視されると言いました。
cpdef Py_ssize_t foo():
raise ValueError("抛个异常")
関数内で手動で ValueError を発生させました。
import pyximport
pyximport.install(language_level=3)
import cython_test
cython_test.foo()
print("正常执行")
"""
ValueError: 抛个异常
Exception ignored in: 'cython_test.foo'
Traceback (most recent call last):
File "...", line 5, in <module>
cython_test.foo()
ValueError: 抛个异常
正常执行
"""
しかし、呼び出されたとき、例外はプログラムを終了しませんでした。
もう 1 つのポイント: いわゆる例外とは、基本的に、プログラムが特定のステップでエラーを起こしたことをインタプリタが発見し、インデックスが範囲外であるなどの例外情報を stderr に書き込み、プログラムを終了することです。
ただし、Cython の場合、戻り値が C 型の場合、例外は無視されます。Python のように例外をスローしたい場合は、Exception 節を渡す必要があります。
cpdef Py_ssize_t foo() except ? -1:
raise ValueError("抛个异常")
現時点では、例外は正しくスローされており、これも期待どおりの結果です。しかし、Cython3.0では、Exception句を使用しなくても、普通に例外がスローされます。
ただし、Except 句は cdef および cpdef で定義された関数でのみ使用できることに注意してください。また、戻り値が C 型の場合にのみ、Except 句を使用する必要があります。戻り値が Python 型の場合は、Except 句を使用する必要はありません (例外は通常どおりスローされます)。使用するとコンパイル エラーが発生します。
たとえば、上記の Py_ssize_t を list に変更し、再コンパイルして何が問題なのかを確認します。
これは、Exception 節を使用した関数が Python オブジェクトを返したことを示しています。
その理由は何なのかと疑問に思う人もいるかもしれません。まず、基になる C 関数が PyObject * を返す場合、通常の実行時に戻り値は正当な Python オブジェクトを指している必要があります。エラーが発生した場合、戻り値は NULL になります。したがって、インタプリタは戻り値が NULL であることを確認すると、エラーがあるに違いないと認識し、開発者にプロンプトを表示するために stderr に例外を出力するだけです。
返される型が C の場合 (インデックスを返すなど)。関数が失敗すると、-1 が (センチネルとして) 返されます。しかし、インタプリタが -1 を見たとき、この -1 が関数の実行時にエラーが発生したときに返されるのか、それとも正常に実行されたときに返されるのかはわかりません。戻り値自体は -1 です。したがって、現時点では、例外バックトラッキング スタックを検出して、その中に例外があるかどうかを確認する必要があります。
Cythonでも同様で、戻り値がPython型であれば戻り値によって例外があるかどうかを判断できます。ただし、戻り値が C 型の場合、戻り値から例外があるかどうかを判断することはできず、excel ? -1 は Cython に戻り値が C 型の場合は例外を確認する必要があることを伝えるためです。バックトラッキングスタックで例外が発生した場合は、スローする必要があります。
上記は Cython0.29 バージョンの実践ですが、Cython3.0 からは、Except 節が必要なくなりました。戻り値が C 型であっても、Cython はバックトレース スタックを検出します。
しかし、ここで疑問が生じます。関数が異常ではないことを保証できるのであれば、例外があるかどうかを確認することはできないのでしょうか? 答えは「はい」です。
cpdef Py_ssize_t foo() noexcept:
pass
noexc 節を通じて、この関数が例外を引き起こさないことを Cython に伝えるため、生成された C コードは例外処理を行いません。ただし、例外が発生した場合、その例外は無視されます。
もう一度強調しますが、Except であっても noExcept であっても、戻り値が C 型である関数に対するものであり、戻り値が Python 型である場合、これら 2 つの句は必要ありません。なぜなら、Python 型の場合、例外が発生したかどうかは戻り値 (NULL かどうか) で判断でき、例外バックトラッキング スタックの検出に追加のコストを費やす必要がありません。
カスタム Numpy ufuncs
Numpy の ufunc (ユニバーサル関数) は、配列に対して要素レベルの演算を実行できる関数を指します。ufunc はベクトル化された演算の中核であり、ループを作成せずに配列の各要素に対して計算を実行できます。
Cython3.0では、Numpyのufuncを簡単にカスタマイズできる新しいデコレータcython.ufuncが追加されました。
cimport cython
import numpy as np
# 装饰成 Numpy 函数时,必须使用 cdef 定义
@cython.ufunc
cdef int add(x, y):
return x + y
arr1 = np.array([1, 2, 3])
arr2 = np.array([1, 2, 3])
print(add(arr1, arr2))
print(np.add(arr1, arr2))
"""
[2 4 6]
[2 4 6]
"""
# 因为规定了返回值是 int 类型,所以浮点数会被强制转化为整数
# 如果希望返回值和输入的 Numpy 数组保持一致,那么将返回值类型指定为 object 即可
arr1 = np.array([1.1, 2.2, 3.3])
arr2 = np.array([1.1, 2.2, 3.3])
print(add(arr1, arr2))
print(np.add(arr1, arr2))
"""
[2 4 6]
[2.2 4.4 6.6]
"""
@cython.ufunc
cdef str to_string(x):
return str(x) * 3
print(to_string(np.array([1, 2, 3])))
"""
['111' '222' '333']
"""
現在のロジックは比較的単純で、Numpy が提供する関数を直接使用できますが、要件がより複雑な場合は、ufunc をカスタマイズする方がはるかに便利です。
cimport cython
import re
import numpy as np
@cython.ufunc
cdef tuple find_ymd(x):
if (match := re.search(r"(\d{4})-(\d{2})-(\d{2})", x)) is not None:
y, m, d = match.groups()
else:
y, m, d = -1, -1, -1
return int(y), int(m), int(d)
dates = np.array([
"2021-01-01",
"2022-02-02",
"2023-03-03",
"0123456789"
])
print(find_ymd(dates))
"""
[(2021, 1, 1) (2022, 2, 2) (2023, 3, 3) (-1, -1, -1)]
"""
なんと、とても単純なことではありませんか?定義された関数は要素レベルでの操作のみを処理する必要があり、ufunc で修飾された後に呼び出されると、配列の各要素に対して自動的に動作します。
注: cython.ufunc は cdef で定義された C 関数のみを装飾できますが、装飾後は Python 外部からアクセスできます。もちろん、Numpy 自体も ufunc を定義する操作を提供していることがわかります。
さらに、Numpy の最下層は C で実装されており、Cython での Numpy の使用も、Numpy が提供する C API に依存していますが、一部の API は現在の Numpy では放棄されています (ただし、まだ使用できます)。
また、Cython3.0 は依然としてデフォルトで古い API を使用しますが、コンパイル時に警告がスローされます。
NPY_NO_DEPRECATED_API マクロを使用すると、これらの警告を抑制し、Numpy 1.7 以降で非推奨となった API を Cython が使用しないようにすることができます。
変動民営化
Python クラスでは、二重アンダースコアで始まり二重アンダースコアで終わらない属性の場合、インタプリタは、それがプライベート属性であることを示すために (実際にはプライベートではありませんが) _ クラス名を先頭に自動的に追加します。しかし、これは Cython によって定義された静的クラスの場合には当てはまらず、Cython が定義する名前はすべて、舞台裏で特別な作業を行わなくても、表示されるものと取得されるものだけです。
cdef class A:
# 外界可以访问、也可以修改
cdef public str __name
# 外界只能访问、但无法修改
cdef readonly int __age
# 外界即无法访问、也无法修改
cdef str address
def __init__(self):
self.__name = "古明地觉"
self.__age = 17
self.address = "地灵殿"
静的クラスの場合、プロパティがプライベートかどうかはパブリックと読み取り専用によって制御されるので、テストしてみましょう。
import pyximport
pyximport.install(language_level=3)
import cython_test
a = cython_test.A()
print(a.__name)
"""
古明地觉
"""
a.__name = "古明地恋"
print(a.__name)
"""
古明地恋
"""
print(a.__age)
"""
17
"""
try:
a.__age = 18
except AttributeError as e:
print(e)
"""
attribute '__age' of 'cython_test.A' objects is not writable
"""
# __name 和 __age 虽然以双下划线开头
# 但对于静态类而言,名称是所见即所得
try:
a.address
except AttributeError as e:
print(e)
"""
'cython_test.A' object has no attribute 'address'
"""
# address 属性因为没有使用 public 或 readonly 对外暴露
# 所以它是绝对的私有,如果不想让外界访问,那么外界是绝对访问不到的
上記は Cython バージョン 0.29 ですが、Cython3.0 であれば、二重アンダースコアで始まる属性については、Python と同様にこっそり名前を置き換えてくれます。
たとえば、上記のファイルを Cython バージョン 3.0 でコンパイルし、テストします。
import pyximport
pyximport.install(language_level=3)
import cython_test
a = cython_test.A()
# 在 Cython3.0 的时候,和 Python 表现一样
# 这里需要通过 _A__name 和 _A_age 访问
print(a._A__name)
"""
古明地觉
"""
print(a._A__age)
"""
17
"""
# 真正的私有,依旧无法访问
try:
a.address
except AttributeError as e:
print(e)
"""
'cython_test.A' object has no attribute 'address'
"""
個人的には、この変更は比較的大きいものの、二重アンダースコアで始まる(二重アンダースコアで終わらない)プロパティやメソッドを外部で使用しないため、影響は少ないと感じています。
揮発性修飾子
C には変数修飾子 volatile があり、マルチスレッド時の変数の可視性を確保する役割を果たします。
CPU がメモリからデータを独自のレジスタに読み取ることはわかっていますが、レジスタと比較すると、CPU がメモリからデータを読み取る速度は十分に速くありません。したがって、レジスタとメモリの間には依然としてキャッシュ、つまり L1、L2、および L3 キャッシュが存在します。
データはメモリ→L3キャッシュ→L2キャッシュ→L1キャッシュ→レジスタの順に転送されますが、同様にCPUがデータを読み込むときは、まずL1キャッシュから探し、データがなければ読み込んでいきます。 L2 キャッシュへの転送、最後にメモリが存在します。
データが下位レベルのキャッシュまたはメモリから読み取られるたびに、次回のアクセスを高速化するために上位レベルのキャッシュにコピーされます。また、各コアには固有の L1 および L2 キャッシュがあり、すべてのコアは L3 キャッシュを共有しますが、変数の可視性の問題がいくつか発生します。
2 つのスレッドによって同時に読み取られる変数 n があり、これら 2 つのスレッドが 2 つのコアで並行して実行されるとします。キャッシュ原理により、変数 n は 2 つのコアの L2 または L1 にキャッシュされるため、読み取り速度は最速になりますが、一方のスレッドがもう一方のスレッドの後の変更を認識できない状況が発生します。 L1 キャッシュと L2 キャッシュは各コアに固有であるためです。
volatile キーワードは、この状況を防ぐ役割を果たします。volatile によって変更された変数の場合、CPU は読み取りが必要になるたびに、少なくとも L3 から読み取り、CPU の計算が完了したらすぐに L3 に書き戻す必要があります。読み書き速度は遅くなりますが、変数が各コアのL1、L2キャッシュで別々に操作されて他のコアが知らないという状況は避けられます。
Cython 3.0 以降、volatile 修飾子がサポートされていますが、0.29 より前はサポートされていませんでした。
# volatile 修饰的必须是 C 类型的变量
cdef volatile int a = 123
C/C++ スレッドを操作しない場合、volatile は基本的に役に立ちません。
まとめ
上記は Cython3.0 におけるより重要な変更点の一部です。これらについては個別に説明する必要があると思います。Cython の内部的な変更点については、まだ細かい変更点がいくつかありますが、実際の使用には影響しないのでここでは割愛しますので、興味のある方は公式サイトをご覧ください。
コーディングレベルでは、Cython0.29 と Cython3.0 の違いはそれほど大きくなく、アップグレード後にあまり多くの調整を行う必要はありません。もちろん、新機能が使用できない場合は、アップグレードする必要はありません。
つまり、代入式、カスタム Numpy ufunc、および volatile 修飾子のサポートは体験する価値があります。
Python猫技術交流会がオープンしました!グループには、国内の一流・二次工場の現役社員や国内外の大学に留学する学生、コーディング歴10年以上のベテランからプログラミング歴1年の新人まで様々な人材が在籍しています。小中学校も始まり、学習環境は良好です!グループに参加したい学生は、公式アカウントの「コミュニケーショングループ」に返信して、毛沢東のWeChatを取得してください(あなたが宣伝パーティーを辞退してください!)~
十分でない?試してみてください
▲ PythonグローバルインタープリタロックGILに関するビッグニュース!
▲ Pythonにおける-mの代表的な使い方と原理解析と開発
▲ Pythonの組み込みzip()の内容をすべてマスターする記事
▲ Pythonで時限タスクを実装するための8つのスキーム!
▲ 8.5K Star! Python コードのメモリ分析のための強力なツール
この記事が役に立ったと思われた場合は、
ぜひシェアして「いいね!」してください、ありがとうございます!