使用 CPython 的生成器函数 api
generator functions using the CPython api
我正在尝试弄清楚如何使用 CPython api 在 C 中编写生成器函数。不幸的是,我不明白该怎么做,而且文档也没有很好地解释它。有人可以解释生成器函数如何在低级代码中工作以及我如何创建它们吗?
类似于
的东西
def gen_func(*args):
for arg in args:
yield arg
首先,PyRun_String
(或任何可以模拟eval
或exec
的东西)当然可以做到,但这似乎是作弊;您不是在 C 中构建生成器函数,而是在 Python 中构建一个,然后在 C 中调用它。
无论如何,您无法弄清楚如何使用 C API 构建生成器函数的原因是没有 C API 可以做到这一点。或者,更确切地说,有一个 C API 从 CPython 框架对象 运行 Python 生成器代码对象构建生成器(并从生成器代码对象构建生成器函数,但是那部分你甚至可以从 Python 做;它只是 types.FunctionType
构造函数),但这对你没有任何好处。 (除非你只想编写为生成器构建 Python 字节码的 C 代码,这与 PyRun_String
一样都是作弊,而且需要更多工作。)
因此,如果您想在 C 中构建生成器函数,则必须手动完成。显然可以做到这一点,正如 Cython 可以做到这一点所证明的那样(达到某个限制——例如,inspect.isgeneratorfunction
和 inspect.isgenerator
将 return False
gen_func
和 gen_func()
)。但这并不容易,我不确定它能给你带来什么。
核心问题是 CPython 通过冻结 CPython 帧并传递它们来实现生成器(因此 API)。 C 代码不使用 CPython 帧,它使用 C 堆栈。 (即使您使用 setjmp
/longjmp
和显式堆栈复制来构建 C 协同程序,您也会与 CPython 本身使用 C 堆栈的方式作斗争。)
所以,我能想到的唯一可行的选择是构建一个迭代器 class(this answer 展示了如何做),然后在其之上实现生成器协议的其余部分.它与在 Python 中实现生成器协议基本相同,但将你的状态存储在 PyObject 结构中而不是在你的对象字典中,就像将任何其他 class 转换为 C.
如果您想了解 Cython 的功能,本质上就是这样,尽管您必须费力浏览大量样板文件才能看到它。创建文件 genpyx.pyx
:
def gen_func():
yield None
然后cythonize genpyx.pyx
,查看创建的genpyx.c
文件。 (寻找 __pyx_gb_6genpyx_2generator
,以及它附近的大多数其他东西。)尽管 Cython 有一种机制可以部分伪造帧,因此它可以通过两端带有 Python 的 Cython 代码进行回溯,但它仍然将所有状态显式存储在它通过函数传递的结构中,就像您必须做的那样。 Cython 确实支持 quasi-documented 生成器属性中的两个 gi_running
和 gi_yieldfrom
,这是个好主意,但它不能伪造 gi_frame
和 gi_code
(任何不仅仅是扩展函数试图伪造 __code__
),而且它不会伪装成 types.GeneratorType
的实例(你可以这样做,但它与任何其他 [=55] 一样危险=]类型)。
与此同时,如果您的模拟生成器没有任何使用 yield
的值,那么实现接受并忽略参数的 send
有什么意义,检查 otherwise-unnecessary first-run 标志被设置,然后做与 __next__
相同的事情?如果你正在构建 Cython 并且需要一些东西来编译 Cython 生成器主体,那么尝试尽可能多地实现生成器协议是必要的,但是 YAGNI 如果你只是手动翻译 gen_func
之类的东西。
我正在尝试弄清楚如何使用 CPython api 在 C 中编写生成器函数。不幸的是,我不明白该怎么做,而且文档也没有很好地解释它。有人可以解释生成器函数如何在低级代码中工作以及我如何创建它们吗?
类似于
的东西def gen_func(*args):
for arg in args:
yield arg
首先,PyRun_String
(或任何可以模拟eval
或exec
的东西)当然可以做到,但这似乎是作弊;您不是在 C 中构建生成器函数,而是在 Python 中构建一个,然后在 C 中调用它。
无论如何,您无法弄清楚如何使用 C API 构建生成器函数的原因是没有 C API 可以做到这一点。或者,更确切地说,有一个 C API 从 CPython 框架对象 运行 Python 生成器代码对象构建生成器(并从生成器代码对象构建生成器函数,但是那部分你甚至可以从 Python 做;它只是 types.FunctionType
构造函数),但这对你没有任何好处。 (除非你只想编写为生成器构建 Python 字节码的 C 代码,这与 PyRun_String
一样都是作弊,而且需要更多工作。)
因此,如果您想在 C 中构建生成器函数,则必须手动完成。显然可以做到这一点,正如 Cython 可以做到这一点所证明的那样(达到某个限制——例如,inspect.isgeneratorfunction
和 inspect.isgenerator
将 return False
gen_func
和 gen_func()
)。但这并不容易,我不确定它能给你带来什么。
核心问题是 CPython 通过冻结 CPython 帧并传递它们来实现生成器(因此 API)。 C 代码不使用 CPython 帧,它使用 C 堆栈。 (即使您使用 setjmp
/longjmp
和显式堆栈复制来构建 C 协同程序,您也会与 CPython 本身使用 C 堆栈的方式作斗争。)
所以,我能想到的唯一可行的选择是构建一个迭代器 class(this answer 展示了如何做),然后在其之上实现生成器协议的其余部分.它与在 Python 中实现生成器协议基本相同,但将你的状态存储在 PyObject 结构中而不是在你的对象字典中,就像将任何其他 class 转换为 C.
如果您想了解 Cython 的功能,本质上就是这样,尽管您必须费力浏览大量样板文件才能看到它。创建文件 genpyx.pyx
:
def gen_func():
yield None
然后cythonize genpyx.pyx
,查看创建的genpyx.c
文件。 (寻找 __pyx_gb_6genpyx_2generator
,以及它附近的大多数其他东西。)尽管 Cython 有一种机制可以部分伪造帧,因此它可以通过两端带有 Python 的 Cython 代码进行回溯,但它仍然将所有状态显式存储在它通过函数传递的结构中,就像您必须做的那样。 Cython 确实支持 quasi-documented 生成器属性中的两个 gi_running
和 gi_yieldfrom
,这是个好主意,但它不能伪造 gi_frame
和 gi_code
(任何不仅仅是扩展函数试图伪造 __code__
),而且它不会伪装成 types.GeneratorType
的实例(你可以这样做,但它与任何其他 [=55] 一样危险=]类型)。
与此同时,如果您的模拟生成器没有任何使用 yield
的值,那么实现接受并忽略参数的 send
有什么意义,检查 otherwise-unnecessary first-run 标志被设置,然后做与 __next__
相同的事情?如果你正在构建 Cython 并且需要一些东西来编译 Cython 生成器主体,那么尝试尽可能多地实现生成器协议是必要的,但是 YAGNI 如果你只是手动翻译 gen_func
之类的东西。