Почему некоторые типы функций "python" на самом деле являются типами?

Многие итераторные "функции" в модуле __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

Да, это связано с тем фактом, что встроенные функции обычно реализуются в C. На самом деле часто код C вводит новые типы вместо простых функций, как в случае enumerate. Написание их на C обеспечивает более точный контроль над ними и часто некоторые улучшения производительности, и поскольку нет реального недостатка, это естественный выбор.

Учтите, что для записи эквивалента:

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

в C, то есть в новом экземпляре генератора, вы должны создать объект кода, который содержит фактический байт-код. Это не невозможно, но это не так проще, чем писать новый тип, который просто реализует __iter__ и __next__, вызывающие C-API Python, а также другие преимущества наличия другого типа.

Итак, в случае enumerate и reversed это просто потому, что он обеспечивает лучшую производительность и более удобен в обслуживании.

Другие преимущества:

  • Вы можете добавлять методы к типу (например, 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 может представлять байт-код функции), поэтому: да, чтобы создать экземпляр PyGen_Type с C, вы должны вручную создать байт-код, и создать не так просто 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)

Обратите внимание, как он содержит аргументы, такие как firstlineno, filename, которые, очевидно, должны быть получены источником python, а не другим кодом C. Очевидно, вы можете создать его на C, но я не уверен, что для этого потребуется меньше символов, чем писать простой новый тип.

Ответ 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 */

Вместо того, как вы создаете новый enumobject, есть функция enum_new, подпись которой не использует фрейм

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

Эта функция помещается в слот tp_new структуры PyEnum_Type (тип PyTypeObject). Здесь мы также видим, что слот tp_iternext занят функцией enum_next, которая содержит простой C-код, который получает следующий элемент итератора, который он перечисляет, и затем возвращает PyObject (кортеж).

Вперед, PyEnum_Type затем помещается во встроенный модуль (Python/bltinmodule.c) с именем enumerate, чтобы он был общедоступным.

Нет байт-кода. Pure C. Гораздо эффективнее любой чистой реализации python или GeneratorType.

Ответ 3

Вызов enumerate должен возвращать итератор. Итератор - это объект с определенным API. Самый простой способ реализации класса с конкретным API - это, как правило, реализовать его как класс.

Причина, по которой он говорит "тип", а не "класс", является специфичным для Python 2, поскольку встроенные классы назывались "типами" в Python 2, так как остальная часть Python имеет оба типа и классы перед Python 2.2. В Python 2.3 классы и типы были унифицированы. И в Python 3 он говорит, что класс:

>>> enumerate
<class 'enumerate'>

Это делает более понятным, что ваш вопрос "Почему некоторые типы встроенных функций вместо функций" имеют очень мало общего с их реализацией в C. Они являются типами/классами, потому что это был лучший способ для реализации функциональности. Это так просто.

Теперь, если мы вместо этого интерпретируем ваш вопрос как "Почему enumerate тип/класс вместо генератора" (это совсем другой вопрос), тогда ответ также естественно отличается. Ответ заключается в том, что генераторы представляют собой ярлыки Python для создания итераторов из функций Python. Они не предназначены для использования с C. Они также менее полезны для создания генераторов из функций, чем из методов класса, как если бы вы хотели создать объект-итератор из метода класса, который необходимо также передать в контексте объекта, но с функцией, которая вам не нужна. Так что в основном это преимущество, которое у вас меньше, чем у "леса".