dis — Desensamblador de bytecodes de Python

Propósito:Convierte objetos de código en una representación legible por humanos de los bytecodes para su análisis.

El módulo dis incluye funciones para trabajar con el bytecode de Python al desensamblarlo en una forma más legible para los humanos. Revisar los bytecodes que ejecuta el intérprete es una buena manera de ajustar a mano los bucles ajustados y realizar otros tipos de optimizaciones. También es útil para encontrar condiciones de carrera en aplicaciones de subprocesos múltiples, ya que se puede usar para estimar el punto en el código donde puede cambiar el control de subprocesos.

Advertencia

El uso de bytecodes es un detalle de implementación específico de la versión del intérprete CPython. Consulte Include/opcode.h en el código fuente de la versión del intérprete que estás utilizando para encontrar la lista canónica de bytecodes.

Desensamblado básico

La función dis() imprime la representación desensamblada de una fuente de código Python (módulo, clase, método, función u objeto de código). Un módulo como dis_simple.py se puede desensamblar ejecutando dis desde la línea de comandos.

dis_simple.py
1
2
3
4
#!/usr/bin/env python3
# encoding: utf-8

my_dict = {'a': 1}

La salida se organiza en columnas con el número de línea de origen original, la dirección de instrucción dentro del objeto de código, el nombre del código de operación y cualquier argumento pasado al código de operación.

$ python3 -m dis dis_simple.py

  4           0 LOAD_CONST               0 ('a')
              2 LOAD_CONST               1 (1)
              4 BUILD_MAP                1
              6 STORE_NAME               0 (my_dict)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE

En este caso, la fuente se traduce en cuatro operaciones diferentes para crear y llenar el diccionario, luego guardar los resultados en una variable local. Dado que el intérprete de Python está basado en la pila, los primeros pasos son colocar las constantes en la pila en el orden correcto con LOAD_CONST, y luego usar BUILD_MAP para extraer la nueva clave y el valor que se agregará al diccionario. El objeto dict resultante está vinculado al nombre my_dict con STORE_NAME.

Desensamblado de funciones

Desafortunadamente, desensamblar un módulo completo no se repite en funciones automáticamente.

dis_function.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env python3
# encoding: utf-8


def f(*args):
    nargs = len(args)
    print(nargs, args)


if __name__ == '__main__':
    import dis
    dis.dis(f)

Los resultados de desensamblar dis_function.py muestran las operaciones para cargar el objeto de código de la función en la pila y luego convertirlo en una función (LOAD_CONST, MAKE_FUNCTION), pero no el cuerpo de la función.

$ python3 -m dis dis_function.py

  5           0 LOAD_CONST               0 (<code object f at
0x1044fcf60, file "dis_function.py", line 5>)
              2 LOAD_CONST               1 ('f')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)

 10           8 LOAD_NAME                1 (__name__)
             10 LOAD_CONST               2 ('__main__')
             12 COMPARE_OP               2 (==)
             14 POP_JUMP_IF_FALSE       34

 11          16 LOAD_CONST               3 (0)
             18 LOAD_CONST               4 (None)
             20 IMPORT_NAME              2 (dis)
             22 STORE_NAME               2 (dis)

 12          24 LOAD_NAME                2 (dis)
             26 LOAD_ATTR                2 (dis)
             28 LOAD_NAME                0 (f)
             30 CALL_FUNCTION            1
             32 POP_TOP
        >>   34 LOAD_CONST               4 (None)
             36 RETURN_VALUE

Para ver dentro de la función, la función en sí debe pasarse a dis().

$ python3 dis_function.py

  6           0 LOAD_GLOBAL              0 (len)
              2 LOAD_FAST                0 (args)
              4 CALL_FUNCTION            1
              6 STORE_FAST               1 (nargs)

  7           8 LOAD_GLOBAL              1 (print)
             10 LOAD_FAST                1 (nargs)
             12 LOAD_FAST                0 (args)
             14 CALL_FUNCTION            2
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Para imprimir un resumen de la función, incluida la información sobre los argumentos y los nombres que usa, llama a show_code(), pasando la función como primer argumento.

#!/usr/bin/env python3
# encoding: utf-8


def f(*args):
    nargs = len(args)
    print(nargs, args)


if __name__ == '__main__':
    import dis
    dis.show_code(f)

El argumento de show_code() se pasa a code_info(), que devuelve un resumen bien formateado de la función, método, cadena de código u otro objeto de código, listo para imprimir.

$ python3 dis_show_code.py

Name:              f
Filename:          dis_show_code.py
Argument count:    0
Kw-only arguments: 0
Number of locals:  2
Stack size:        3
Flags:             OPTIMIZED, NEWLOCALS, VARARGS, NOFREE
Constants:
   0: None
Names:
   0: len
   1: print
Variable names:
   0: args
   1: nargs

Clases

Las clases se pueden pasar a dis(), en cuyo caso todos los métodos se desensamblan a su vez.

dis_class.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
# encoding: utf-8

import dis


class MyObject:
    """Example for dis."""

    CLASS_ATTRIBUTE = 'some value'

    def __str__(self):
        return 'MyObject({})'.format(self.name)

    def __init__(self, name):
        self.name = name


dis.dis(MyObject)

Los métodos se enumeran en orden alfabético, no en el orden en que aparecen en el archivo.

$ python3 dis_class.py

Disassembly of __init__:
 16           0 LOAD_FAST                1 (name)
              2 LOAD_FAST                0 (self)
              4 STORE_ATTR               0 (name)
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE

Disassembly of __str__:
 13           0 LOAD_CONST               1 ('MyObject({})')
              2 LOAD_ATTR                0 (format)
              4 LOAD_FAST                0 (self)
              6 LOAD_ATTR                1 (name)
              8 CALL_FUNCTION            1
             10 RETURN_VALUE

Código fuente

A menudo es más conveniente trabajar con el código fuente de un programa que con los propios objetos de código. Las funciones en dis aceptan argumentos de cadena que contienen código fuente y los convierten en objetos de código antes de producir el desensamblado u otra salida.

dis_string.py
import dis

code = """
my_dict = {'a': 1}
"""

print('Disassembly:\n')
dis.dis(code)

print('\nCode details:\n')
dis.show_code(code)

Pasar una cadena le permite guardar el paso de compilar el código y mantener una referencia a los resultados usted mismo, lo cual es más conveniente en los casos en que se examinan declaraciones fuera de una función.

$ python3 dis_string.py

Disassembly:

  2           0 LOAD_CONST               0 ('a')
              2 LOAD_CONST               1 (1)
              4 BUILD_MAP                1
              6 STORE_NAME               0 (my_dict)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE

Code details:

Name:              <module>
Filename:          <disassembly>
Argument count:    0
Kw-only arguments: 0
Number of locals:  0
Stack size:        2
Flags:             NOFREE
Constants:
   0: 'a'
   1: 1
   2: None
Names:
   0: my_dict

Uso del desensamblado para depurar

A veces, al depurar una excepción, puede ser útil ver qué bytecode causó un problema. Hay un par de formas de desensamblar el código en torno a un error. La primera es mediante el uso de dis() en el intérprete interactivo para informar sobre la última excepción. Si no se pasa ningún argumento a dis(), entonces busca una excepción y muestra el desensamblado de la parte superior de la pila que lo causó.

$ python3
Python 3.5.1 (v3.5.1:37a07cee5969, Dec  5 2015, 21:12:44)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> j = 4
>>> i = i + 4
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'i' is not defined
>>> dis.dis()
  1 -->       0 LOAD_NAME                0 (i)
              3 LOAD_CONST               0 (4)
              6 BINARY_ADD
              7 STORE_NAME               0 (i)
             10 LOAD_CONST               1 (None)
             13 RETURN_VALUE
>>>

El --> después del número de línea indica el código de operación que causó el error. No hay una variable i definida, por lo que el valor asociado con el nombre no se puede cargar en la pila.

Un programa también puede imprimir la información sobre un rastreo activo pasándolo directamente a distb(). En este ejemplo, hay una excepción DivideByZero, pero dado que la fórmula tiene dos divisiones, puede que no esté claro qué parte es cero.

dis_traceback.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/usr/bin/env python3
# encoding: utf-8

i = 1
j = 0
k = 3

try:
    result = k * (i / j) + (i / k)
except Exception:
    import dis
    import sys
    exc_type, exc_value, exc_tb = sys.exc_info()
    dis.distb(exc_tb)

El error es fácil de detectar cuando se carga en la pila en la versión desensamblada. La operación incorrecta se resalta con -->, y la línea anterior empuja el valor de j en la pila.

    $ python3 dis_traceback.py

      4           0 LOAD_CONST               0 (1)
                  2 STORE_NAME               0 (i)

      5           4 LOAD_CONST               1 (0)
                  6 STORE_NAME               1 (j)

      6           8 LOAD_CONST               2 (3)
                 10 STORE_NAME               2 (k)

      8          12 SETUP_EXCEPT            24 (to 38)

      9          14 LOAD_NAME                2 (k)
                 16 LOAD_NAME                0 (i)
                 18 LOAD_NAME                1 (j)
        -->      20 BINARY_TRUE_DIVIDE
                 22 BINARY_MULTIPLY
                 24 LOAD_NAME                0 (i)
                 26 LOAD_NAME                2 (k)
                 28 BINARY_TRUE_DIVIDE
                 30 BINARY_ADD
                 32 STORE_NAME               3 (result)

...recortado...

Análisis de rendimiento de bucles

Además de los errores de depuración, dis también puede ayudar a identificar problemas de rendimiento. Examinar el código desensamblado es especialmente útil con bucles estrechos donde el número de instrucciones de Python es bajo pero se traducen en un conjunto ineficiente de bytecodes. La utilidad del desensamblado se puede ver al examinar algunas implementaciones diferentes de una clase, Diccionary, que lee una lista de palabras y las agrupa por su primera letra.

dis_test_loop.py
import dis
import sys
import textwrap
import timeit

module_name = sys.argv[1]
module = __import__(module_name)
Dictionary = module.Dictionary

dis.dis(Dictionary.load_data)
print()
t = timeit.Timer(
    'd = Dictionary(words)',
    textwrap.dedent("""
    from {module_name} import Dictionary
    words = [
        l.strip()
        for l in open('/usr/share/dict/words', 'rt')
    ]
    """).format(module_name=module_name)
)
iterations = 10
print('TIME: {:0.4f}'.format(t.timeit(iterations) / iterations))

La aplicación del controlador de prueba dis_test_loop.py se puede usar para ejecutar cada encarnación de la clase Diccionary, comenzando con una implementación sencilla pero lenta.

dis_slow_loop.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env python3
# encoding: utf-8


class Dictionary:

    def __init__(self, words):
        self.by_letter = {}
        self.load_data(words)

    def load_data(self, words):
        for word in words:
            try:
                self.by_letter[word[0]].append(word)
            except KeyError:
                self.by_letter[word[0]] = [word]

La ejecución del programa de prueba con esta versión muestra el programa desensamblado y la cantidad de tiempo que tarda en ejecutarse.

$ python3 dis_test_loop.py dis_slow_loop

 12           0 SETUP_LOOP              83 (to 86)
              3 LOAD_FAST                1 (words)
              6 GET_ITER
        >>    7 FOR_ITER                75 (to 85)
             10 STORE_FAST               2 (word)

 13          13 SETUP_EXCEPT            28 (to 44)

 14          16 LOAD_FAST                0 (self)
             19 LOAD_ATTR                0 (by_letter)
             22 LOAD_FAST                2 (word)
             25 LOAD_CONST               1 (0)
             28 BINARY_SUBSCR
             29 BINARY_SUBSCR
             30 LOAD_ATTR                1 (append)
             33 LOAD_FAST                2 (word)
             36 CALL_FUNCTION            1 (1 positional, 0
keyword pair)
             39 POP_TOP
             40 POP_BLOCK
             41 JUMP_ABSOLUTE            7

 15     >>   44 DUP_TOP
             45 LOAD_GLOBAL              2 (KeyError)
             48 COMPARE_OP              10 (exception match)
             51 POP_JUMP_IF_FALSE       81
             54 POP_TOP
             55 POP_TOP
             56 POP_TOP

 16          57 LOAD_FAST                2 (word)
             60 BUILD_LIST               1
             63 LOAD_FAST                0 (self)
             66 LOAD_ATTR                0 (by_letter)
             69 LOAD_FAST                2 (word)
             72 LOAD_CONST               1 (0)
             75 BINARY_SUBSCR
             76 STORE_SUBSCR
             77 POP_EXCEPT
             78 JUMP_ABSOLUTE            7
        >>   81 END_FINALLY
             82 JUMP_ABSOLUTE            7
        >>   85 POP_BLOCK
        >>   86 LOAD_CONST               0 (None)
             89 RETURN_VALUE

TIME: 0.0568

La salida anterior muestra dis_slow_loop.py que tarda 0.0568 segundos en cargar las 235886 palabras en la copia de /usr/share/dict/words en OS X. Eso no es tan malo, pero el desensamblado adjunto muestra que el ciclo está haciendo más trabajo del que necesita. Cuando ingresa al bucle en el código de operación 13, configura un contexto de excepción (SETUP_EXCEPT). Luego, se necesitan seis códigos de operación para encontrar self.by_letter [word[0]] antes de agregar word a la lista. Si hay una excepción porque word[0] aún no está en el diccionario, el controlador de excepciones hace todo el mismo trabajo para determinar word[0] (tres códigos de operación) y establece self.by_letter[word[0]] a una nueva lista que contiene la palabra.

Una técnica para eliminar la configuración de excepción es rellenar previamente self.by_letter con una lista para cada letra del alfabeto. Eso significa que siempre se debe encontrar la lista para la nueva palabra, y el valor se puede guardar después de la búsqueda.

dis_faster_loop.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
# encoding: utf-8

import string


class Dictionary:

    def __init__(self, words):
        self.by_letter = {
            letter: []
            for letter in string.ascii_letters
        }
        self.load_data(words)

    def load_data(self, words):
        for word in words:
            self.by_letter[word[0]].append(word)

El cambio reduce el número de códigos de operación a la mitad, pero solo reduce el tiempo a 0.0567 segundos. Obviamente, el manejo de excepciones tuvo algunos gastos generales, pero no una cantidad significativa.

$ python3 dis_test_loop.py dis_faster_loop

 17           0 SETUP_LOOP              38 (to 41)
              3 LOAD_FAST                1 (words)
              6 GET_ITER
        >>    7 FOR_ITER                30 (to 40)
             10 STORE_FAST               2 (word)

 18          13 LOAD_FAST                0 (self)
             16 LOAD_ATTR                0 (by_letter)
             19 LOAD_FAST                2 (word)
             22 LOAD_CONST               1 (0)
             25 BINARY_SUBSCR
             26 BINARY_SUBSCR
             27 LOAD_ATTR                1 (append)
             30 LOAD_FAST                2 (word)
             33 CALL_FUNCTION            1 (1 positional, 0
keyword pair)
             36 POP_TOP
             37 JUMP_ABSOLUTE            7
        >>   40 POP_BLOCK
        >>   41 LOAD_CONST               0 (None)
             44 RETURN_VALUE

TIME: 0.0567

El rendimiento se puede mejorar aún más moviendo la búsqueda de self.by_letter fuera del bucle (el valor no cambia, después de todo).

dis_fastest_loop.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env python3
# encoding: utf-8

import collections


class Dictionary:

    def __init__(self, words):
        self.by_letter = collections.defaultdict(list)
        self.load_data(words)

    def load_data(self, words):
        by_letter = self.by_letter
        for word in words:
            by_letter[word[0]].append(word)

Los códigos de operación 0-6 ahora encuentran el valor de self.by_letter y lo guardan como una variable local by_letter. El uso de una variable local solo toma un único código de operación, en lugar de dos (la declaración 22 usa LOAD_FAST para colocar el diccionario en la pila). Después de este cambio, el tiempo de ejecución ha bajado a 0.0473 segundos.

$ python3 dis_test_loop.py dis_fastest_loop

 14           0 LOAD_FAST                0 (self)
              3 LOAD_ATTR                0 (by_letter)
              6 STORE_FAST               2 (by_letter)

 15           9 SETUP_LOOP              35 (to 47)
             12 LOAD_FAST                1 (words)
             15 GET_ITER
        >>   16 FOR_ITER                27 (to 46)
             19 STORE_FAST               3 (word)

 16          22 LOAD_FAST                2 (by_letter)
             25 LOAD_FAST                3 (word)
             28 LOAD_CONST               1 (0)
             31 BINARY_SUBSCR
             32 BINARY_SUBSCR
             33 LOAD_ATTR                1 (append)
             36 LOAD_FAST                3 (word)
             39 CALL_FUNCTION            1 (1 positional, 0
keyword pair)
             42 POP_TOP
             43 JUMP_ABSOLUTE           16
        >>   46 POP_BLOCK
        >>   47 LOAD_CONST               0 (None)
             50 RETURN_VALUE

TIME: 0.0473

Una optimización adicional, sugerida por Brandon Rhodes, es eliminar por completo la versión de Python del ciclo for. Si itertools.groupby() se usa para organizar la entrada, la iteración se mueve a C. Esto es seguro porque se sabe que las entradas están ordenadas. Si ese no fuera el caso, el programa necesitaría ordenarlos primero.

dis_eliminate_loop.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
# encoding: utf-8

import operator
import itertools


class Dictionary:

    def __init__(self, words):
        self.by_letter = {}
        self.load_data(words)

    def load_data(self, words):
        # Arrange by letter
        grouped = itertools.groupby(
            words,
            key=operator.itemgetter(0),
        )
        # Save arranged sets of words
        self.by_letter = {
            group[0][0]: group
            for group in grouped
        }

La versión de itertools tarda solo 0.0332 segundos en ejecutarse, aproximadamente el 60% del tiempo de ejecución del original.

    $ python3 dis_test_loop.py dis_eliminate_loop

     16           0 LOAD_GLOBAL              0 (itertools)
                  3 LOAD_ATTR                1 (groupby)

     17           6 LOAD_FAST                1 (words)
                  9 LOAD_CONST               1 ('key')

     18          12 LOAD_GLOBAL              2 (operator)
                 15 LOAD_ATTR                3 (itemgetter)
                 18 LOAD_CONST               2 (0)
                 21 CALL_FUNCTION            1 (1 positional, 0
    keyword pair)
                 24 CALL_FUNCTION          257 (1 positional, 1
    keyword pair)
                 27 STORE_FAST               2 (grouped)

     21          30 LOAD_CONST               3 (<code object
    <dictcomp> at 0x101517930, file ".../dis_eliminate_loop.py",
line 21>)
                 33 LOAD_CONST               4
    ('Dictionary.load_data.<locals>.<dictcomp>')
                 36 MAKE_FUNCTION            0

     23          39 LOAD_FAST                2 (grouped)
                 42 GET_ITER
                 43 CALL_FUNCTION            1 (1 positional, 0
    keyword pair)
                 46 LOAD_FAST                0 (self)
                 49 STORE_ATTR               4 (by_letter)
                 52 LOAD_CONST               0 (None)
                 55 RETURN_VALUE

    TIME: 0.0332

Optimizaciones del compilador

Desensamblar la fuente compilada también expone algunas de las optimizaciones realizadas por el compilador. Por ejemplo, las expresiones literales se pliegan durante la compilación, cuando es posible.

dis_constant_folding.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env python3
# encoding: utf-8

# Folded
i = 1 + 2
f = 3.4 * 5.6
s = 'Hello,' + ' World!'

# Not folded
I = i * 3 * 4
F = f / 2 / 3
S = s + '\n' + 'Fantastic!'

Ninguno de los valores en las expresiones en las líneas 5-7 puede cambiar la forma en que se realiza la operación, por lo que el resultado de las expresiones se puede calcular en el momento de la compilación y colapsar en instrucciones individuales LOAD_CONST. Eso no es cierto sobre las líneas 10-12. Debido a que una variable está involucrada en esas expresiones, y la variable puede referirse a un objeto que sobrecarga al operador involucrado, la evaluación debe retrasarse al tiempo de ejecución.

$ python3 -m dis dis_constant_folding.py

  5           0 LOAD_CONST              11 (3)
              2 STORE_NAME               0 (i)

  6           4 LOAD_CONST              12 (19.04)
              6 STORE_NAME               1 (f)

  7           8 LOAD_CONST              13 ('Hello, World!')
             10 STORE_NAME               2 (s)

 10          12 LOAD_NAME                0 (i)
             14 LOAD_CONST               6 (3)
             16 BINARY_MULTIPLY
             18 LOAD_CONST               7 (4)
             20 BINARY_MULTIPLY
             22 STORE_NAME               3 (I)

 11          24 LOAD_NAME                1 (f)
             26 LOAD_CONST               1 (2)
             28 BINARY_TRUE_DIVIDE
             30 LOAD_CONST               6 (3)
             32 BINARY_TRUE_DIVIDE
             34 STORE_NAME               4 (F)

 12          36 LOAD_NAME                2 (s)
             38 LOAD_CONST               8 ('\n')
             40 BINARY_ADD
             42 LOAD_CONST               9 ('Fantastic!')
             44 BINARY_ADD
             46 STORE_NAME               5 (S)
             48 LOAD_CONST              10 (None)
             50 RETURN_VALUE

Ver también