Почему некоторые типы функций "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. Они также менее полезны для создания генераторов из функций, чем из методов класса, как если бы вы хотели создать объект-итератор из метода класса, который необходимо также передать в контексте объекта, но с функцией, которая вам не нужна. Так что в основном это преимущество, которое у вас меньше, чем у "леса".