12

模块中的许多迭代器“函数”__builtin__实际上是作为类型实现的,即使文档将它们称为“函数”。举个例子enumerate。该文档说它相当于:

def enumerate(sequence, start=0):
    n = start
    for elem in sequence:
        yield n, elem
        n += 1

当然,这正是我会实施的。但是,我使用之前的定义运行了以下测试,并得到了这个:

>>> x = enumerate(range(10))
>>> x
<generator object enumerate at 0x01ED9F08>

这是我所期望的。但是,在使用该__builtin__版本时,我得到了这个:

>>> x = enumerate(range(10))
>>> x
<enumerate object at 0x01EE9EE0>

由此我推断它被定义为

class enumerate:
    def __init__(self, sequence, start=0):
        # ....

    def __iter__(self):
        # ...

而不是文档显示的标准形式。现在我可以理解它是如何工作的,以及它是如何等同于标准形式的,我想知道这样做的原因是什么。这种方式效率更高吗?它是否与在 C 中实现的这些函数有关(我不知道它们是否是,但我怀疑是这样)?

我正在使用 Python 2.7.2,以防差异很重要。

提前致谢。

4

3 回答 3

9

是的,这与内置函数通常在 C 中实现这一事实有关。实际上,C 代码通常会引入新类型而不是普通函数,例如enumerate. 用 C 语言编写它们可以更好地控制它们,并且通常会提高一些性能,并且由于没有真正的缺点,这是一个自然的选择。

考虑到编写相当于:

def enumerate(sequence, start=0):
    n = start
    for elem in sequence:
        yield n, elem
        n += 1

在 C 中,即生成器的新实例,您应该创建一个包含实际字节码的代码对象。这不是不可能的,但并不比编写一个简单地实现__iter____next__调用 Python C-API 的新类型容易,再加上拥有不同类型的其他优点。

所以,在 and 的情况下enumeratereversed这仅仅是因为它提供了更好的性能,并且更易于维护。

其他优点包括:

  • 您可以将方法添加到类型(例如chain.from_iterable)。即使使用函数也可以做到这一点,但是您必须先定义它们,然后手动设置属性,这看起来不太干净。
  • 您可以isinstance在可迭代对象上使用我们。这可能允许一些优化(例如,如果您知道isinstance(iterable, itertools.repeat),那么您可能能够优化代码,因为您知道将产生哪些值。

编辑:只是为了澄清我的意思:

在 C 中,即生成器的新实例,您应该创建一个包含实际字节码的代码对象。

查看Objects/genobject.c创建PyGen_Type实例的唯一函数是PyGen_New其签名是:

PyObject *
PyGen_New(PyFrameObject *f)

现在,Objects/frameobject.c我们可以看到要创建一个PyFrameObjectyou must call PyFrame_New,它具有以下签名:

PyFrameObject *
PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
            PyObject *locals)

如您所见,它需要一个PyCodeObject实例。PyCodeObjects 是python解释器如何在内部表示字节码(例如 aPyCodeObject可以表示函数的字节码),所以:是的,PyGen_Type要从 C 创建一个实例,您必须手动创建字节码,而且创建 s 并不容易,PyCodeObject因为PyCode_New有这个签名:

PyCodeObject *
PyCode_New(int argcount, int kwonlyargcount,
           int nlocals, int stacksize, int flags,
           PyObject *code, PyObject *consts, PyObject *names,
           PyObject *varnames, PyObject *freevars, PyObject *cellvars,
           PyObject *filename, PyObject *name, int firstlineno,
           PyObject *lnotab)

请注意它如何包含诸如 之类的参数firstlinenofilename这些参数显然是由 python 源而不是从其他 C 代码中获取的。显然,您可以在 C 中创建它,但我完全不确定它是否需要比编写简单的新类型更少的字符。

于 2013-02-13T19:39:00.323 回答
2

是的,它们是用 C 实现的。它们将 C API 用于迭代器(PEP 234),其中迭代器是通过创建具有tp_iternext插槽的新类型来定义的。

由生成器函数语法 ( yield) 创建的函数是返回特殊生成器对象的“神奇”函数。这些是 的实例types.GeneratorType,您无法手动创建。如果使用 C API 的不同库定义了自己的迭代器类型,它不会是 的实例GeneratorType,但仍会实现 C API 迭代器协议。

因此,该enumerate类型是不同于 的独特类型,GeneratorType您可以像使用任何其他类型一样使用它,isinstance例如(尽管您不应该)。


与 Bakuriu 的答案不同,enumerate它不是生成器,因此没有字节码/帧。

$ grep -i 'frame\|gen' Objects/enumobject.c
    PyObject_GenericGetAttr,        /* tp_getattro */
    PyType_GenericAlloc,            /* tp_alloc */
    PyObject_GenericGetAttr,        /* tp_getattro */
    PyType_GenericAlloc,            /* tp_alloc */

相反,您创建新枚举对象的方式是使用 function enum_new,其签名不使用框架

static PyObject *
enum_new(PyTypeObject *type, PyObject *args, PyObject *kwds)

这个函数被放置在结构(类型)的tp_new槽内。在这里,我们还看到该槽被函数占用,该函数包含简单的 C 代码,该代码获取它正在枚举的迭代器的下一项,然后返回一个 PyObject(一个元组)。PyEnum_TypePyTypeObjecttp_iternextenum_next

继续前进,PyEnum_Type然后将其放入Python/bltinmodule.c名为 的内置模块 ( ) 中enumerate,以便可以公开访问。

不需要字节码。纯 C。比任何纯 python 或generatortype实现都高效得多。

于 2013-02-13T20:02:39.207 回答
1

enumerate调用需要返回一个迭代器。迭代器是具有特定 API 的对象。使用特定 API 实现类的最简单方法通常是将其实现为类。

它说“类型”而不是“类”的原因是 Python 2 特定的,因为内置类在 Python 2 中被称为“类型”,因为 Python 的其余部分在 Python 2.2 之前同时具有类型和类。在 Python 2.3 中,类和类型是统一的。因此,在 Python 3 中,它表示类:

>>> enumerate
<class 'enumerate'>

这更清楚地表明您的问题“为什么某些内置类型而不是函数”与它们在 C 中实现几乎没有关系。它们是类型/类,因为这是实现功能的最佳方式。就这么容易。

现在,如果我们将您的问题解释为“为什么是enumerate类型/类而不是生成器”(这是一个非常不同的问题),那么答案自然也会不同。答案是生成器是从 Python 函数创建迭代器的 Python 快捷方式。它们不适合从 C 中使用。它们对于从函数中生成生成器的用处也比在类方法中生成生成器的用处小,就好像你想从你还需要传入对象上下文的类方法中创建一个迭代器对象一样,但有了一个功能,你就不需要这个了。所以这主要是你有更少的“脚手架”代码的好处。

于 2013-09-28T20:44:57.027 回答