Подтвердить что ты не робот

Как я должен понимать вывод dis.dis?

Я хотел бы понять, как использовать dis (диссиплятор байт-кода Python). В частности, как интерпретировать вывод dis.dis (или dis.disassemble)?

.

Вот очень конкретный пример (в Python 2.7.3):

dis.dis("heapq.nsmallest(d,3)")

      0 BUILD_SET             24933
      3 JUMP_IF_TRUE_OR_POP   11889
      6 JUMP_FORWARD          28019 (to 28028)
      9 STORE_GLOBAL          27756 (27756)
     12 LOAD_NAME             29811 (29811)
     15 STORE_SLICE+0  
     16 LOAD_CONST            13100 (13100)
     19 STORE_SLICE+1

Я вижу, что JUMP_IF_TRUE_OR_POP и т.д. являются инструкциями байткода (хотя интересно, BUILD_SET не отображается в этом списке, хотя я ожидаю он работает как BUILD_TUPLE). Я думаю, что номера в правой части являются распределениями памяти, а числа слева - goto номера... Я замечаю они почти увеличиваются на 3 каждый раз (но не совсем).

Если я заключу dis.dis("heapq.nsmallest(d,3)") внутри функции:

def f_heapq_nsmallest(d,n):
    return heapq.nsmallest(d,n)

dis.dis("f_heapq(d,3)")

      0 BUILD_TUPLE            26719
      3 LOAD_NAME              28769 (28769)
      6 JUMP_ABSOLUTE          25640
      9 <44>                                      # what is <44> ?  
     10 DELETE_SLICE+1 
     11 STORE_SLICE+1 
4b9b3361

Ответ 1

Вы пытаетесь разобрать строку, содержащую исходный код, но не поддерживаемую dis.dis в Python 2. С помощью строкового аргумента она обрабатывает строку так, как будто она содержит байтовый код (см. функцию disassemble_string в dis.py). Таким образом, вы видите бессмысленный вывод, основанный на неверном истолковании исходного кода в виде байтового кода.

В Python 3 разные вещи, где dis.dis компилирует строковый аргумент перед его дизассемблированием:

Python 3.2.3 (default, Aug 13 2012, 22:28:10) 
>>> import dis
>>> dis.dis('heapq.nlargest(d,3)')
  1           0 LOAD_NAME                0 (heapq) 
              3 LOAD_ATTR                1 (nlargest) 
              6 LOAD_NAME                2 (d) 
              9 LOAD_CONST               0 (3) 
             12 CALL_FUNCTION            2 
             15 RETURN_VALUE         

В Python 2 вам нужно самому скомпилировать код, прежде чем передать его в dis.dis:

Python 2.7.3 (default, Aug 13 2012, 18:25:43) 
>>> import dis
>>> dis.dis(compile('heapq.nlargest(d,3)', '<none>', 'eval'))
  1           0 LOAD_NAME                0 (heapq)
              3 LOAD_ATTR                1 (nlargest)
              6 LOAD_NAME                2 (d)
              9 LOAD_CONST               0 (3)
             12 CALL_FUNCTION            2
             15 RETURN_VALUE        

Что означают цифры? Число 1 в крайнем левом углу - это номер строки в исходном коде, с которого был скомпилирован этот байтовый код. Числа в столбце слева - это смещение инструкции в байт-коде, а числа справа - opargs. Давайте посмотрим на фактический байтовый код:

>>> co = compile('heapq.nlargest(d,3)', '<none>', 'eval')
>>> co.co_code.encode('hex')
'6500006a010065020064000083020053'

При смещении 0 в байтовом коде мы находим 65, код операции для LOAD_NAME, с oparg 0000; то (при смещении 3) 6a - код операции LOAD_ATTR, с 0100 oparg и т.д. Обратите внимание, что opargs находятся в порядке little-endian, так что 0100 - это номер 1. Недокументированный модуль opcode содержит таблицы opname, дающие вам имя для каждого кода операции, а opmap дает вам код операции для каждого имя:

>>> opcode.opname[0x65]
'LOAD_NAME'

Значение oparg зависит от кода операции, и для полной истории вам нужно прочитать реализацию виртуальной машины CPython в ceval.c. Для LOAD_NAME и LOAD_ATTR oparg является индексом в свойстве co_names объекта кода:

>>> co.co_names
('heapq', 'nlargest', 'd')

Для LOAD_CONST это индекс в свойстве co_consts объекта кода:

>>> co.co_consts
(3,)

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

Ответ 2

Я публикую свой ответ на другой вопрос, чтобы быть уверенным, что найду его во время dis.dis().


Чтобы завершить великолепный ответ Гарета Риса, вот лишь небольшая колонка-сводка, чтобы объяснить вывод дизассемблированного байт-кода.

Например, учитывая эту функцию:

def f(num):
    if num == 42:
        return True
    return False

Это может быть разобрано в (Python 3.6):

(1)|(2)|(3)|(4)|          (5)         |(6)|  (7)
---|---|---|---|----------------------|---|-------
  2|   |   |  0|LOAD_FAST             |  0|(num)
   |-->|   |  2|LOAD_CONST            |  1|(42)
   |   |   |  4|COMPARE_OP            |  2|(==)
   |   |   |  6|POP_JUMP_IF_FALSE     | 12|
   |   |   |   |                      |   |
  3|   |   |  8|LOAD_CONST            |  2|(True)
   |   |   | 10|RETURN_VALUE          |   |
   |   |   |   |                      |   |
  4|   |>> | 12|LOAD_CONST            |  3|(False)
   |   |   | 14|RETURN_VALUE          |   |

Каждый столбец имеет определенное назначение:

  1. Соответствующий номер строки в исходном коде
  2. Опционально указывает текущую выполненную инструкцию (например, когда байт-код поступает из объекта кадра)
  3. Метка, которая обозначает возможный JUMP из более ранней инструкции к этой
  4. Адрес в байт-коде, который соответствует байтовому индексу (это кратные 2, потому что Python 3.6 использует 2 байта для каждой инструкции, хотя в предыдущих версиях он мог отличаться)
  5. Имя инструкции (также называемое opname), каждая из которых кратко объяснена в модуле dis и их реализация может быть найдена в ceval.c (основной цикл CPython)
  6. Аргумент (если есть) инструкции, который используется внутри Python для извлечения некоторых констант или переменных, управления стеком, перехода к определенной инструкции и т.д.
  7. Дружественное толкование аргумента инструкции