【问题标题】:How to write C code (of a Python module) able to return a Python iterator object?如何编写能够返回 Python 迭代器对象的 C 代码(Python 模块的)?
【发布时间】:2017-09-23 04:56:35
【问题描述】:

在我很快成功地用 C++ (see here) 编写了一个简约的 Python3.6 扩展模块之后,我计划提供一个与以下 Python 函数 iterUniqueCombos() 相同的 Python 模块:

def iterUniqueCombos(lstOfSortableItems, sizeOfCombo):
    lstOfSortedItems = sorted(lstOfSortableItems)
    sizeOfList = len(lstOfSortedItems)

    lstComboCandidate = []

    def idxNextUnique(idxItemOfList):
        idxNextUniqueCandidate = idxItemOfList + 1

        while (
                idxNextUniqueCandidate < sizeOfList 
                    and 
                lstOfSortedItems[idxNextUniqueCandidate] == lstOfSortedItems[idxItemOfList]
        ): # while
            idxNextUniqueCandidate += 1

        idxNextUnique = idxNextUniqueCandidate

        return idxNextUnique

    def combinate(idxItemOfList):
        if len(lstComboCandidate) == sizeOfCombo:
            yield tuple(lstComboCandidate)
        elif sizeOfList - idxItemOfList >= sizeOfCombo - len(lstComboCandidate):
            lstComboCandidate.append(lstOfSortedItems[idxItemOfList])
            yield from combinate(idxItemOfList + 1)
            lstComboCandidate.pop()
            yield from combinate(idxNextUnique(idxItemOfList))

    yield from combinate(0)

我对 Python 和 C++ 编程有一些基本了解,但完全不知道如何将 Python yield“翻译”成 Python 扩展模块的 C++ 代码。所以我的问题是:

如何编写能够返回 Python 迭代器对象的(Python 模块的)C++ 代码?

欢迎任何让我入门的提示。

更新(状态 2017-05-07):

评论:yield 没有 C++ 等价物。我将从在 Python 中手动实现迭代器协议开始,以摆脱思维定势的产量和产量。 – user2357112 Apr 26 at 1:16danny 答案中的提示 这个问题的答案与询问“如何在不使用 yield 的情况下实现迭代器”相同,但在 C++ 中扩展而不是纯 Python。 通过重写算法代码以消除 yield 并从头开始编写 Python 扩展模块的 C 代码(什么导致下雨Segmentation Fault 错误)。

我目前在这个问题上的最新知识是,使用Cython 可以将上面的 Python 代码(使用yield)直接翻译成 C Python 扩展模块的代码。

这不仅可以按原样使用 Python 代码(无需重写任何内容),而且除此之外,Cython 使用 yield 的算法创建的扩展模块的速度至少运行两次与使用 __iter____next__ 重写算法从迭代器类创建的扩展模块一样快(如果没有将 Cython 特定的速度优化代码添加到 Python 脚本中,则后者有效)

【问题讨论】:

标签: python c python-3.x iterator python-c-extension


【解决方案1】:

这更像是对您的问题编辑的回应,而不是完整的答案 - 我同意 Danny 回答的要点,即您需要使用 __next__/next 方法在一个类中实现这一点(取决于版本蟒蛇)。在您的编辑中,您断言它必须是可能的,因为 Cython 可以做到。我认为值得看看 Cython 是如何做到这一点的。

从一个基本示例开始(之所以选择它是因为它有几个不同的yield 语句和一个循环):

def basic_iter(n):
    a = 0
    b = 5
    yield a
    a+=3
    yield b
    b+=2

    for i in range(n):
        yield a+b+n
        a = b+1
        b*=2
    yield 50

Cython 所做的第一件事是定义一个 __pyx_CoroutineObject C 类,该类具有实现 __next__/next__Pyx_Generator_Next 方法。 __pyx_CoroutineObject的几个相关属性:

  • body - 实现您定义的逻辑的 C 函数指针。
  • resume_label - 一个整数,用于记住你在 body 定义的函数中的距离
  • closure - 一个自定义创建的 C 类,用于存储 body 中使用的所有变量。

__Pyx_Generator_Next 以一种稍微迂回的方式调用了 body 属性,这是您定义的 Python 代码的翻译。

然后让我们看看分配给body 的函数是如何工作的——以我的示例__pyx_gb_5iters_2generator 为例。它做的第一件事是使用resume_label 跳转到右边的yield 语句:

switch (__pyx_generator->resume_label) {
    case 0: goto __pyx_L3_first_run;
    case 1: goto __pyx_L4_resume_from_yield;
    case 2: goto __pyx_L5_resume_from_yield;
    case 3: goto __pyx_L8_resume_from_yield;
    case 4: goto __pyx_L9_resume_from_yield;
    default: /* CPython raises the right error here */
    __Pyx_RefNannyFinishContext();
    return NULL;
  }

任何变量赋值都是通过closure结构完成的(本地命名为__pyx_cur_scope

/*     a = 0             # <<<<<<<<<<<<<< */
__pyx_cur_scope->__pyx_v_a = __pyx_int_0

yield 设置resume_label 并返回(resume_label 允许您下次直接跳回去):

__pyx_generator->resume_label = 1;
return __pyx_r;

循环稍微复杂一些,但基本相同——它使用goto 跳转到C 循环(这是合法的)。

最后,一旦到达终点,就会引发StopIteration 错误:

PyErr_SetNone(PyExc_StopIteration);

总之,Cython 完全按照您的建议去做:它使用 __next__next 方法定义了一个类,并使用该类来跟踪状态。因为它是自动化的,所以它非常擅长跟踪引用计数,从而避免了您遇到的Segmentation Fault 错误。使用goto 返回上一个执行点是高效的,但需要小心。

我明白为什么用单个 __next__/next 函数在 C 中重写您的生成器函数是不吸引人的,而且 Cython 显然提供了一种直接的方法,无需自己编写 C,但它不使用在你已经被告知的基础上进行翻译的任何特殊技术。

【讨论】:

  • 你写:Cython clearly offers a straightforward way of doing it without writing C yourself, but it doesn't use any special techniques to do the translation on top of what you've already been told. 同时(如果我理解你的答案正确的话),解释 Cython 用来将 Python yield 转换为适当的 C 等效项的特殊技术有人告诉我:使用 switch 到 goto 语句,其中迭代状态保存在特殊的 closure 结构中。
  • 你理解的没错。我认为我的观点是,没有什么是你自己写不出来的。我怀疑如果你给 C 程序员写一个可以多次调用的函数来像 Python 生成器一样工作的问题,他们可能会想出类似的东西(也许避免goto...)。无论如何 - 我主要认为值得看看 Cython 是如何做到的。它要么显示一个有用的复制机制,要么让你相信你不想自己写它,你应该只使用 Cython(这两个都是好的结果)
  • 在这种情况下,了解 Python 解释器如何在它自己的 C 代码中执行它肯定会很有趣。如果 Cython 只是复制 Python 自己的工作方式,我不会感到惊讶。
  • 我能看到的最佳答案是stackoverflow.com/questions/24501406/…。我认为大部分信息都保存在框架对象中,函数和生成器的处理方式没有太大区别(Python 仍然需要存储当前行号和它们中的任何一个的局部变量,所以它是' 不太难能够从一个点恢复 - 例如,这也用于调试器)。
【解决方案2】:

Python 中的迭代器是生成器的一种特殊形式,由包含方法__iter__next 的类实现,其中__iter__ 返回selfnext 依次返回每个值,引发@987654327 @ 在迭代结束时 - see PEP.

要提供 C++ 等效项,C++ 代码需要实现相同的 Python 函数以符合协议。生成的扩展类型是一个迭代器。

换句话说,这个问题的答案与询问“如何在不使用 yield 的情况下使用 C++ 扩展而不是纯 Python 的情况下实现迭代器”相同。关于堆栈溢出,有几个现有的答案。

NB - next 在 Python 3 上是 __next__

【讨论】:

  • 答案直接回答了如何实现迭代器的问题,而不是评论说“实现 PEP 中描述的协议”。所以我觉得它更清晰,更合适。如果您的问题不再适用,或者您认为其他问题已被涵盖,请考虑锁定/更新它。
  • 见答案的最后一行。
  • 我同意审稿人的结论。答案是有效的,并且您的答案不包含上述答案中尚不存在的其他相关信息。
猜你喜欢
  • 2015-09-06
  • 1970-01-01
  • 1970-01-01
  • 2011-05-17
  • 1970-01-01
  • 2018-02-05
  • 1970-01-01
  • 1970-01-01
  • 2011-01-19
相关资源
最近更新 更多