Как выполняются mylist.reverse () и list.reverse (mylist)? - PullRequest
6 голосов
/ 14 марта 2020

Предположительно оба mylist.reverse() и list.reverse(mylist) в конечном итоге выполнят reverse_slice в listobject.c через list_reverse_impl или PyList_Reverse. Но как они на самом деле туда добраться? Каковы пути от выражений Python к коду C в этом файле C? Что их связывает? И какие из этих двух обратных функций (если таковые имеются) они go через?

Обновление для награды: ответ Димитриса (Обновление 2: я имел в виду оригинальную версию, перед ней был расширен), и комментарии под ним объясняют части, но я все еще скучаю по нескольким вещам и хотел бы получить исчерпывающий ответ.

  • Как соотносятся два пути из двух Python выражения сходятся? Если я правильно понимаю, разбор и обсуждение байт-кода и того, что происходит со стеком, особенно LOAD_METHOD, прояснит это. (Как это было сделано в комментариях к ответу Димитриса.)
  • Какой «несвязанный метод» помещен в стек? Это «C функция» (какая?) Или «Python объект»?
  • Как я могу сказать, что это list_reverse функция в файле listobject.c.h? Я не думаю, что интерпретатор Python похож на «давайте посмотрим на файл, который звучит похоже, и на функцию, которая звучит похоже» * . Я скорее подозреваю, что тип list определен где-то и как-то «зарегистрирован» под именем «list», и что функция reverse «зарегистрирована» под именем «reverse» (может быть, это то, что макрос LIST_REVERSE_METHODDEF делает?).
  • Меня не интересует (по этому вопросу) фреймы стека, обработка аргументов и тому подобные вещи (так что, возможно, не так много из того, что происходит внутри call_function). Меня действительно интересует то, что я сказал изначально: путь от выражений Python к этому C коду в этом файле C. И желательно, как я могу найти такие пути в целом.

Чтобы объяснить мою мотивацию: Для другой вопрос Я хотел знать, какой код C работает, когда я звоню list.reverse(mylist). Я был довольно уверен, что нашел его, просматривая и ища имена. Но я хочу быть более уверенным и в целом лучше понимать связи.

1 Ответ

6 голосов
/ 14 марта 2020

PyList_Reverse является частью C -API, вы бы назвали его, если бы манипулировали списками Python в C, он не используется ни в одном из двух случаев.

Они оба go - list_reverse_impl (на самом деле list_reverse, который охватывает list_reverse_impl), что является функцией C, которая реализует как list.reverse, так и list_instance.reverse.

Оба вызова обрабатываются call_function в ceval, после чего создается сгенерированный для них код операции CALL_METHOD (операторы dis.dis видят его). call_function претерпел значительные изменения в Python 3.8 (с введением PEP 590 ), так что то, что происходит оттуда, вероятно, слишком велико для предмета, чтобы в него можно было ответить в одном вопросе .

Дополнительные вопросы:

Как сходятся два пути из двух выражений python? Если я правильно понимаю, разбор и обсуждение байт-кода и того, что происходит со стеком, особенно LOAD_METHOD, прояснит это.

Начнем после того, как оба выражения скомпилированы в соответствующие представления байт-кода:

l = [1, 2, 3, 4]

Случай A, для l.reverse() имеем:

1           0 LOAD_NAME                0 (l)
            2 LOAD_METHOD              1 (reverse)
            4 CALL_METHOD              0
            6 RETURN_VALUE

Случай B, для list.reverse(l) у нас есть:

1           0 LOAD_NAME                0 (list)
            2 LOAD_METHOD              1 (reverse)
            4 LOAD_NAME                2 (l)
            6 CALL_METHOD              1
            8 RETURN_VALUE

Мы можем смело игнорировать код операции RETURN_VALUE, здесь не имеет значения

Давайте сосредоточимся на отдельных реализациях для каждого кода операции, а именно LOAD_NAME, LOAD_METHOD и CALL_METHOD. Мы можем видеть, что помещается в стек значений , просматривая, какие операции вызываются для него. (Обратите внимание, что оно инициализируется, чтобы указывать на стек значений, расположенный внутри объекта кадра для каждого выражения.)

LOAD_NAME:

Что выполняется в этом случае это довольно просто. Учитывая наше имя, l или list в каждом случае (каждое имя находится в `co-> co_names, кортеже, в котором хранятся имена, которые мы используем внутри объекта кода), шаги:

  1. Ищите имя внутри locals. Если найдено, go до 4.
  2. Найдите имя внутри globals. Если найдено, go до 4.
  3. Найдите имя внутри builtins. Если найдено, go до 4.
  4. Если найдено, значение pu sh, обозначаемое именем в стеке. Иначе, NameError.

В случае A имя l встречается в глобальных переменных. В случае B он находится во встроенных файлах. Итак, после LOAD_NAME стек выглядит следующим образом:

Дело A: stack_pointer -> [1, 2, 3, 4]

Дело B: stack_pointer -> <type list>

LOAD_METHOD:

Во-первых, мне не следует, чтобы этот код операции генерировался, только если выполняется доступ к атрибуту (т. Е. obj.attr). Вы также можете получить метод и вызвать его через a = obj.attr, а затем a(), но это приведет к генерации кода операции CALL_FUNCTION (подробнее см. Далее).

После загрузки имени При вызове (reverse в обоих случаях) мы ищем объект в верхней части стека (либо [1, 2, 3, 4], либо list) для метода с именем reverse. Это делается с помощью _PyObject_GetMethod, его документация гласит:

Возвращает 1, если метод найден, 0, если это обычный атрибут из __dict__ или что-то, возвращаемое с помощью протокол дескриптора.

Метод встречается только в случае A, когда мы обращаемся к атрибуту (reverse) через экземпляр объекта списка. В случае B вызываемый объект возвращается после вызова протокола дескриптора, поэтому возвращаемое значение равно 0 (но мы, конечно, возвращаем объект!).

Здесь мы расходимся с возвращаемым значением:

В случае A:

SET_TOP(meth);
PUSH(obj);  // self

У нас есть SET_TOP, за которым следует PUSH. Мы переместили метод на вершину стека, а затем снова набрали значение sh. В этом случае stack_pointer теперь выглядит:

stack_pointer -> [1, 2, 3, 4]
                 <reverse method of lists>

В случае B мы имеем:

SET_TOP(NULL);
Py_DECREF(obj);
PUSH(meth);

Снова SET_TOP, за которым следует PUSH. Счетчик ссылок obj (т.е. list) уменьшен, потому что, насколько я могу судить, он больше не нужен. В этом случае стек теперь выглядит так:

stack_pointer -> <reverse method of lists>
                 NULL

Для случая B у нас есть дополнительный LOAD_NAME. Следуя предыдущим шагам, стек для дела B теперь становится:

stack_pointer -> [1, 2, 3, 4]
                 <reverse method of lists>
                 NULL

Довольно похоже.

CALL_METHOD:

Это не ' не вносить никаких изменений в стек. В обоих случаях вызывается call_function с передачей состояния потока, указателя стека и количества позиционных аргументов (oparg).

Единственное отличие состоит в выражении, используемом для передачи позиционных аргументов.

Для случая A нам нужно учесть неявный self, который должен быть вставлен в качестве первого позиционного аргумента. Поскольку сгенерированный для него код операции не сигнализирует о том, что позиционный аргумент был передан (поскольку ни один из них не был передан явно):

4 CALL_METHOD              0

мы вызываем call_function с oparg + 1 = 0 + 1 = 1, чтобы сигнализировать об этом одном позиционном Аргумент существует в стеке ([1, 2, 3, 4]).

В случае B, где мы явно передаем экземпляр в качестве первого аргумента, это учитывается:

6 CALL_METHOD              1

, поэтому вызов call_function может сразу передать oparg в качестве значение для позиционных аргументов.

Что такое «несвязанный метод», помещенный в стек? Это «C функция» (какая?) Или «Python объект»?

Это Python объект, который оборачивается вокруг C функции. Объект Python является дескриптором метода, а функция C, которую он переносит, - list_reverse.

Все встроенные методы и функции реализованы в C. Во время инициализации CPython инициализирует все встроенные функции (см. list здесь ) и добавляет оболочки для всех методов . Эти оболочки (объекты) являются дескрипторами, которые используются для реализации Методы и Функции .

Когда метод извлекается из класса через один из его экземпляров, он считается связанным с этим экземпляром. Это можно увидеть, посмотрев на присвоенный ему атрибут __self__:

m = [1, 2, 3, 4].reverse
m()                        # use __self__ 
print(m.__self__)          # [4, 3, 2, 1]

Этот метод все еще можно вызывать даже без экземпляра, который его квалифицирует. Это связано с этим экземпляром. (ПРИМЕЧАНИЕ. Это обрабатывается кодом операции CALL_FUNCTION, а не LOAD/CALL_METHOD).

Несвязанный метод - это метод, который еще не связан с экземпляром. list.reverse не связан, он ожидает вызова через экземпляр для привязки к нему.

То, что не связано, не означает, что его нельзя вызвать, list.reverse вызывается просто отлично, если вы явно передаете аргумент self в качестве аргумента. Помните, что методы - это просто специальные функции, которые (помимо прочего) неявно передают self в качестве первого аргумента после привязки к экземпляру.

Как я могу сказать, что это функция list_reverse в файл listobject. c .h?

Это просто, вы можете увидеть, как методы списка инициализируются в listobject.c. LIST_REVERSE_METHODDEF - это просто макрос, который после замены добавляет функцию list_reverse в этот список. tp_methods списка затем заключаются в объекты функций, как указано ранее.

Здесь все может показаться сложным, потому что CPython использует внутренний инструмент, аргумент clini c, для автоматизации обработки аргументов. Это как бы двигает определения, слегка запутывая.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...