[Версия Python, описанная в этой статье, — 3.x, точнее — альфа-версия CPython 3.3.]
Концепция вызываемого является фундаментальной в Python. Размышляя о том, что можно «назвать», сразу очевидным ответом являются функции. Будь то пользовательские функции (написанные вами) или встроенные функции (наиболее вероятно реализованные в C внутри интерпретатора CPython), функции должны были вызываться, верно?
Ну, есть и методы, но они не очень интересны, потому что это просто специальные функции, которые связаны с объектами. Что еще можно назвать? Вы можете или не можете быть знакомы со способностью вызывать объекты , если они принадлежат классам, которые определяют магический метод __call__. Таким образом, объекты могут действовать как функции. И если подумать об этом, классы тоже могут быть вызваны. В конце концов, вот как мы создаем новые объекты:
class Joe: ... [contents of class] joe = Joe()
Здесь мы «зовем» Джо, чтобы создать новый экземпляр. Так что классы также могут выступать в качестве функций!
Оказывается, что все эти концепции хорошо объединены в реализации CPython. Все в Python — это объект, который включает в себя все сущности, описанные в предыдущих абзацах (пользовательские и встроенные функции, методы, объекты, классы). Все эти звонки обслуживаются одним механизмом. Этот механизм элегантен и не так сложен для понимания, поэтому о нем стоит знать. Но давайте начнем с самого начала.
Компиляция звонков
CPython выполняет нашу программу в два основных этапа:
- Исходный код Python скомпилирован в байт-код.
- Виртуальная машина выполняет этот байт-код, используя набор инструментов из встроенных объектов и модулей, чтобы помочь ей выполнить свою работу.
В этом разделе я приведу краткий обзор того, как первый шаг относится к совершению звонков. Я не буду вдаваться в подробности, так как эти детали не очень интересная часть, на которой я хочу остановиться в этой статье. Если вы хотите узнать больше о потоке исходного кода Python в компиляторе, прочитайте это .
Вкратце, компилятор Python идентифицирует все, что следует (аргументы …) внутри выражения, как вызов [1] . Узлом AST для этого является Call. Компилятор испускает код для Call в функции compiler_call в Python / compile.c. В большинстве случаев инструкция байт-кода CALL_FUNCTION будет отправлена. Есть некоторые варианты, которые я собираюсь игнорировать для целей статьи. Например, если у вызова есть «звездные аргументы» — func (a, b, * args), есть специальная инструкция для обработки этого — CALL_FUNCTION_VAR. Это и другие специальные инструкции — просто вариации на одну и ту же тему.
call_function
Таким образом, CALL_FUNCTION — это инструкция, на которой мы собираемся сосредоточиться. Вот что он делает :
Call_function (ARGC)
Вызывает функцию. Младший байт argc указывает количество позиционных параметров, старший байт — число ключевых параметров. В стеке код операции сначала находит параметры ключевого слова. Для каждого ключевого аргумента значение находится над ключом. Ниже параметров ключевого слова позиционные параметры находятся в стеке, причем самый правый параметр находится сверху. Ниже параметров вызываемый объект функции находится в стеке. Выдает все аргументы функции и саму функцию из стека и возвращает возвращаемое значение.
Байт-код CPython оценивается функцией мамонта PyEval_EvalFrameEx в Python / ceval.c. Функция пугающая, но это не более чем модный диспетчер кодов операций. Он читает инструкции из объекта кода данного фрейма и выполняет их. Вот, например, обработчик для CALL_FUNCTION (немного очищен для удаления макросов трассировки и синхронизации):
TARGET(CALL_FUNCTION) { PyObject **sp; sp = stack_pointer; x = call_function(&sp, oparg); stack_pointer = sp; PUSH(x); if (x != NULL) DISPATCH(); break; }
Не так уж плохо — это на самом деле очень читабельно. call_function выполняет фактический вызов (мы его немного рассмотрим), oparg — это числовой аргумент инструкции, а stack_pointer указывает на вершину стека [2] . Значение, возвращаемое call_function, помещается обратно в стек, а DISPATCH — это просто некоторая магия макроса для вызова следующей инструкции.
call_function также находится в Python / ceval.c. Это реализует фактическую функциональность инструкции. В 80 строк это не очень долго, но достаточно долго, поэтому я не буду вставлять его целиком. Вместо этого я объясню поток в целом и вставлю небольшие фрагменты, где это уместно; Вы можете следовать за кодом, открытым в вашем любимом редакторе.
Любой вызов — это просто вызов объекта
Самый важный первый шаг в понимании того, как вызовы работают в Python, — это игнорировать большую часть того, что делает call_function. Да, я имею в виду Подавляющее большинство кода в этой функции имеет дело с оптимизацией для различных распространенных случаев. Его можно удалить, не повредив правильности интерпретатора, только его работоспособности. Если мы пока игнорируем все оптимизации, все, что делает call_function, декодирует количество аргументов и количество аргументов ключевого слова из единственного аргумента CALL_FUNCTION и перенаправляет его в do_call. Мы вернемся к оптимизациям позже, поскольку они интересны, но пока давайте посмотрим, каков основной поток.
do_call загружает аргументы из стека в объекты PyObject (кортеж для позиционных аргументов, dict для аргументов ключевого слова), самостоятельно выполняет трассировку и оптимизацию, но в конечном итоге вызывает PyObject_Call.
PyObject_Call — это очень важная функция. Он также доступен для расширений в Python C API. Вот оно во всей красе:
PyObject * PyObject_Call(PyObject *func, PyObject *arg, PyObject *kw) { ternaryfunc call; if ((call = func->ob_type->tp_call) != NULL) { PyObject *result; if (Py_EnterRecursiveCall(" while calling a Python object")) return NULL; result = (*call)(func, arg, kw); Py_LeaveRecursiveCall(); if (result == NULL && !PyErr_Occurred()) PyErr_SetString( PyExc_SystemError, "NULL result without error in PyObject_Call"); return result; } PyErr_Format(PyExc_TypeError, "'%.200s' object is not callable", func->ob_type->tp_name); return NULL; }
Помимо глубокой защиты от рекурсии и обработки ошибок [3] , PyObject_Call извлекает атрибут tp_call [4] типа объекта и вызывает его. Это возможно, поскольку tp_call содержит указатель на функцию.
Позвольте этому погрузиться на мгновение. Это он . Игнорирование всевозможных замечательных оптимизаций, вот что все вызовы в Python сводятся к:
- Все в Python является объектом [5] .
- Каждый объект имеет тип; тип объекта определяет, что можно сделать с / с объектом.
- Когда объект вызывается, вызывается атрибут его типа tp_call.
Как пользователь Python, вы можете напрямую взаимодействовать с tp_call только тогда, когда хотите, чтобы ваши объекты вызывались. Если вы определяете свой класс в Python, вы должны реализовать для этого метод __call__. Этот метод напрямую отображается в tp_call с помощью CPython. Если вы определяете свой класс как расширение C, вы должны назначить tp_call в объекте типа вашего класса вручную.
Но помните, что сами классы «вызываются» для создания новых объектов, поэтому tp_call также играет здесь свою роль. Еще более существенно то, что когда вы определяете класс, в него также входит вызов — в метаклассе класса. Это интересная тема, и я расскажу о ней в следующей статье.
Дополнительный кредит: Оптимизация в CALL_FUNCTION
Эта часть не является обязательной, поскольку основной пункт статьи был доставлен в предыдущем разделе. Тем не менее, я думаю, что этот материал интересен, поскольку он содержит примеры того, как некоторые вещи, которые вы обычно не воспринимаете как объекты, на самом деле являются объектами в Python.
Как я упоминал ранее, мы могли бы просто использовать PyObject_Call для каждого CALL_FUNCTION и покончить с этим. В действительности имеет смысл провести некоторые оптимизации, чтобы охватить общие случаи, когда это может быть излишним. PyObject_Call — очень универсальная функция, которая нуждается во всех своих аргументах в специальных объектах кортежей и словарей (для позиционных и ключевых аргументов соответственно). Эти аргументы должны быть взяты из стека и расположены в контейнерах, ожидаемых PyObject_Call. В некоторых распространенных случаях мы можем избежать многих из этих накладных расходов, и это то, что касается оптимизации в call_function.
Первый адрес особого случая call_function:
/* Always dispatch PyCFunction first, because these are presumed to be the most frequent callable object. */ if (PyCFunction_Check(func) && nk == 0) {
Это обрабатывает объекты типа builtin_function_or_method (представленные типом PyCFunction в реализации C). В Python их много, как отмечается в комментарии выше. Все функции и методы, реализованные в C, будь то интерпретатор CPython или расширения C, попадают в эту категорию. Например:
>>> type(chr) <class 'builtin_function_or_method'> >>> type("".split) <class 'builtin_function_or_method'> >>> from pickle import dump >>> type(dump) <class 'builtin_function_or_method'>
Есть дополнительное условие: если количество аргументов ключевого слова, переданных функции, равно нулю. Это позволяет некоторые важные оптимизации. Если рассматриваемая функция не принимает никаких аргументов (помеченных флагом METH_NOARGS при создании функции) или только один аргумент объекта (флаг METH_0), call_function не проходит обычную упаковку аргументов и может напрямую вызывать указатель на базовую функцию. Чтобы понять, как это возможно, настоятельно рекомендуется прочитать о PyCFunction и флагах METH_ в этой части документации .
Далее, есть некоторая специальная обработка для методов классов, написанных на Python:
else { if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
PyMethod — это внутренний объект, используемый для представления связанных методов . Особенность методов заключается в том, что они содержат ссылку на объект, к которому они привязаны. call_function извлекает этот объект и помещает его в стек, готовясь к тому, что будет дальше.
Вот остальная часть кода вызова (после него в call_object есть только некоторая очистка стека):
if (PyFunction_Check(func)) x = fast_function(func, pp_stack, n, na, nk); else x = do_call(func, pp_stack, na, nk);
do_call мы уже встречали — он реализует наиболее общую форму вызова. Однако есть еще одна оптимизация — если func — это PyFunction (объект, используемый внутри для представления функций, определенных в коде Python), используется отдельный путь — fast_function.
Чтобы понять, что делает fast_function, важно сначала рассмотреть, что происходит, когда выполняется функция Python. Проще говоря, его объект кода оценивается (с самим PyEval_EvalCodeEx). Этот код ожидает, что его аргументы находятся в стеке. Следовательно, в большинстве случаев нет смысла упаковывать аргументы в контейнеры и распаковывать их снова. С некоторой осторожностью их можно просто оставить в стеке и сэкономить много драгоценных циклов ЦП.
Все остальное возвращается к do_call. Это, кстати, включает в себя объекты PyCFunction , что делать есть аргументы ключевых слов. Любопытный аспект этого факта заключается в том, что несколько эффективнее не передавать аргументы ключевых слов в функции C, которые либо принимают их, либо подходят только для позиционных аргументов. Например, [6] :
$ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"' 's.split(";")' 1000000 loops, best of 3: 0.3 usec per loop $ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"' 's.split(sep=";")' 1000000 loops, best of 3: 0.469 usec per loop
Это большая разница, но вход очень маленький. Для больших строк разница практически не заметна:
$ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"*1000' 's.split(";")' 10000 loops, best of 3: 98.4 usec per loop $ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"*1000' 's.split(sep=";")' 10000 loops, best of 3: 98.7 usec per loop
Резюме
Цель этой статьи состояла в том, чтобы обсудить, что значит быть вызываемым в Python, подходя к этой концепции с самого низкого возможного уровня — подробностей реализации виртуальной машины CPython. Лично я считаю эту реализацию очень элегантной, поскольку она объединяет несколько концепций в одну. Как показал дополнительный раздел о кредитах, сущности Python, которые мы обычно не рассматриваем как объекты — функции и методы — на самом деле являются объектами и также могут обрабатываться одинаково. Как я и обещал, будущие статьи будут глубже погружаться в значение tp_call для создания новых объектов и классов Python.
[2] | Виртуальная машина CPython — это стековая машина . |
[4] | Под «атрибутом» здесь я подразумеваю поле структуры (иногда также называемое «слот» в документации). Если вы совершенно не знакомы с тем, как определяются расширения Python C, перейдите по этой странице . |