为什么一些Python内置的“函数”实际上是类型?

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, 如果差异很重要,请告诉我。
提前感谢您。

1
这对你是个问题吗?函数和类只是可调用对象... - JBernardo
@JBernardo 在几乎所有情况下这不是问题(而且当它是问题时,你应该修复破坏的黑客)。但这仍然很有趣。 - user395760
4
不,当然不是。这只是一个学术性的问题。我想了解实施它们背后的原理,因为实现生成器非常容易。也许这会让我对这个问题有些见解:我是否应该用自己的生成器来做到这一点? - Alejandro Piad
3个回答

9

是的,这与内置函数通常是用C语言实现有关。通常情况下,C代码会引入新类型而不是普通函数,例如enumerate。 用C编写它们可以更好地控制它们,并且通常还可以提高性能, 而且没有任何实际的缺点,这是一个自然的选择。

请注意,要编写等价于以下内容的代码:

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

在C中,即创建“生成器”的新实例,您应当创建一个包含实际字节码的代码对象。这并非不可能,但比编写简单实现Python C-API调用__iter____next__的新类型更加困难,而且具有不同类型的其他优势。
因此,在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 文件,我们可以看到要创建一个 PyFrameObject,必须调用 PyFrame_New 函数,其签名如下:

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

正如您所看到的,它需要一个PyCodeObject实例。PyCodeObject是Python解释器在内部表示字节码的方式(例如,PyCodeObject可以表示函数的字节码),因此:是的,要从C创建PyGen_Type实例,您必须手动创建字节码。而且创建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语言中创建它,但我并不确定它所需的字符数是否比编写一个简单的新类型少。

4
为什么用C语言编写的函数必须成为新类型? - martineau
1
@forivall 你可能跳过了我的回答开头:“在很多情况下,C代码会引入新的类型而不是普通函数,就像枚举一样”。 - Bakuriu
1
@forivall 提到这可能与它们是用C实现有关。我解释了为什么将 enumerate 写成函数而不是实现为新类型并不是一个好主意,因此它确实与 OP 的疑虑相关。 - Bakuriu
是的,创建新类型最简单的方法就是实现一个新类型,而不是调用PyFrame_New。但是你为什么认为需要调用PyFrame_New来在C中实现函数呢? - Lennart Regebro
我不同意这就是OP所问的。你的回答解释了为什么在C中将enumerate实现为类/类型比生成器更容易,但它并没有回答OP实际提出的通用问题。 - Lennart Regebro
显示剩余10条评论

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 */

相反,创建新的枚举对象的方法是使用函数enum_new,其签名不使用帧。
static PyObject *
enum_new(PyTypeObject *type, PyObject *args, PyObject *kwds)

该函数被放置在PyEnum_Type结构体的tp_new插槽中(类型为PyTypeObject)。在这里,我们还可以看到tp_iternext插槽由enum_next函数占用,该函数包含直接的C代码,获取迭代器正在枚举的下一个项目,然后返回一个PyObject(元组)。
接下来,PyEnum_Type被放置在内置模块(Python/bltinmodule.c)中,并命名为enumerate,以便公开访问。
无需字节码。纯C。比任何纯Python或generatortype实现都更有效率。

我从未说过enumerate需要“bytecode”或帧对象。我说的是创建GeneratorType的新实例需要这样做,如果enumerate作为返回GeneratorType实例的函数实现,则也需要这样做。 - Bakuriu
1
@Bakuriu,我指责你说了这样的话。你的回答致力于在C中定义生成器。但是没有人这样做,我们在C中定义自定义迭代器类型。 - forivall
1
我的回答致力于回答以下问题:1)为什么enumerate不是一个简单的生成器,而是一种新类型;2)它是否与使用C语言有关。提问者从未询问enumerate的实际实现方式。 - Bakuriu
1
OP从未询问过enumerate的实际实现方式。是的。"为什么enumerate不是一个简单的生成器而是一个新类型"不是。他问为什么它们不是“函数”。我解释说,生成器是由特殊的函数构造的,这些函数不是手动创建的(只有在使用'yield'函数编写Python时才有意义)。然后你解释了如何在C中编写生成器。多么愚蠢。所以我解释了'enumerate'是如何在C中编写的,以及它不需要成为生成器。 - forivall
+1. 我想知道为什么这个答案只有那么少的+1-在我看来,它更准确地回答了问题。 - glglgl
显示剩余2条评论

1
< p > enumerate 调用需要返回一个迭代器。迭代器是具有特定API的对象。通常实现具有特定API的类的最简单方法是将其实现为类。 < p > 之所以说“类型”而不是“类”是Python 2特定的,因为在Python 2中内置类被称为“类型”,这是Python 2.2之前Python同时拥有类型和类的遗留问题。在Python 2.3中,类和类型被统一了。因此,在Python 3中,它说的是类:

>>> enumerate
<class 'enumerate'>

这使得你的问题“为什么一些内置类型是类而不是函数”与它们在C中的实现关系很小更加清晰。它们是类型/类,因为这是实现功能的最佳方式。就这么简单。
现在,如果我们将你的问题解释为“为什么enumerate是类型/类而不是生成器”(这是一个非常不同的问题),那么答案也自然不同。答案是生成器是Python使用Python函数创建迭代器的快捷方式。它们不适用于从C中使用。对于将类方法转换为生成器比将函数转换为生成器更有用,因为如果您想要从类方法创建迭代器对象,则需要同时传递对象上下文,但是对于函数则不需要。因此,这主要是减少了“脚手架”代码的好处。

我不明白Python3/Python2的区别与OP的问题有什么关系(因为他只提到了Python2.7)。此外,“它们是类型/类,因为这是实现功能的最佳方式”是显而易见的,否则就意味着Python开发人员喜欢浪费时间以困难的方式做事情而没有任何优势。OP的问题更具体。 - Bakuriu
@Bakuriu:关键是Python 2将其称为“types”使人们认为这与它们在C中的实现有关,正如其他两个回答所表明的那样。这是错误的。这与它们在C中的实现无关。这在Python 3中很明显,因为它们不再是类型,而是类。 - Lennart Regebro
@Bakuriu,我澄清了问题。 - Lennart Regebro
我相信OP非常清楚这一点。实际上请注意“从这里我推断它被定义为class enumerate: ...”。这里没有C代码。此外,OP本人承认他不知道它们是否是用C实现的。他在问:“为什么enumerate(sequence)enumerate的实例而不是generator的实例?这可能与它可能是用C实现有关吗?”至少这就是我回答时从问题中读到的内容,我认为用C实现一个问题,正如我所解释的那样。 - Bakuriu
@Bakuriu,OP知道的不如回答OP实际发布的问题重要,因为SO应该是通用有用的。 - Lennart Regebro
@LennartRegebro 我们回答问题是在问题正文底部发布的,而不是在标题中。也许标题问题有点误导或可能存在歧义 - 为了消除歧义,您可以将其替换为“为什么一些Python内置的“函数”实际上是类型“type”,而不是类型“function”?”或“而不是类型“class”?”,或者具体地说,“为什么内置的enumerate是类型“type”,而不是(...)并返回“enumerate”对象而不是“generator”对象?”---(<-编辑:这就是你的答案)实际上,我们应该将这个讨论移到元数据上。 - forivall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接