El día de hoy les vengo a hablar de la palabra del señor de Python bytecode y un poco del proceso de compilado e interpretado de este lenguaje.

Primero que nada hay que entender que en Python todo es un objeto por lo tanto variables, funciones, clases, etc… tienen propiedades y podemos hacer cosas como asignar una función a un nombre, ejemplo:


>>> def foo(x):
...    y = 3
...    return x * y

var_name = foo

o podemos pasar una función como parámetro a otra función:


>>> def foo(x):
...    y = 3
...    return x * y

>>> def bar(foo):
...    z = 5
...    return foo(z)

O podemos invocar la función si hacer uso de ella:


>>> foo
<function foo at 0x7f058c254cf8>

También podemos ver sus atributos:


>>> dir(foo)
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']

Entre estos atributos podemos ver uno en particular que es func_code:


>>> foo.func_code
<code object foo at 0x7f058c2971b0, file "<stdin>", line 1>

func_code, contiene el code object de esta función y tiene diferentes propiedades con las cual podemos mostrar ciertas partes del código:


>>> dir(foo.func_code)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']

Por ejemplo podemos ver las variables y las constantes:


>>> foo.func_code.co_varnames
('x', 'y')
>>> foo.func_code.co_consts
(None, 3)

Antes de pasar a jugar con el bytecode, recordemos que Python es compilado e interpretado y pasa por varias pasos antes de retornar un resultado, a grandes rasgos hace estos cuatro pasos:

Lexing: Linea por linea la parte en pedazos y los convierte en tokens.

Parsing: Toma cada uno de esos token y los convierte en una estructura relacionada llamada Abstract Syntax Tree.

Compiling: El compilador toma el AST y lo convierte en code objects.

Interpreting: Y por ultimo el interpretador toma esos code objects y los ejecuta.

Esto es una explicación muy por encima, pero si quieren algo mas profundo y detallado pueden buscar libros específicamente de como funcionan los compiladores y programar su propio compilador, que es lo que posiblemente haga yo por que no se mucho del tema.

Bueno, regresemos al tema del bytecode. Bytecode es la representación interna de nuestro código echa por el compilador y ejecutado por nuestro interpretador, en este caso cpython (si no saben que implementación de python están usando, lo mas seguro es que estén usando cpython).

Para poder ver el bytecode de nuestra funcion foo que hicimos al principio hacemos lo siguiente:


>>> foo.func_code.co_code
'd\x01\x00}\x01\x00|\x00\x00|\x01\x00\x14S'

Eso que nos retorna co_code es nuestro bytecode y cada uno de estos bytes los tomara el interpretador y hará lo que tenga que hacer, pero somos humanos así que convirtamos esto a enteros:


>>> [ord(b) for b in foo.func_code.co_code]
[100, 1, 0, 125, 1, 0, 124, 0, 0, 124, 1, 0, 20, 83]

Cada uno de estos enteros representa una instrucción que podemos encontrar aquí:

https://github.com/python/cpython/blob/master/Lib/opcode.py

Como pueden ver en opcode.py vemos que 100 es LOAD_CONST Y 1 es POP_TOP y así sucesivamente, esto sigue siendo poco humano y para esto tenemos el modulo dis, este modulo es un desensamblador de python bytecode que nos permite hacer análisis del mismo, por ejemplo desensamblemos nuestra función de un principio:


>>> import dis
>>> dis.dis(foo)
  2           0 LOAD_CONST               1 (3)
              3 STORE_FAST               1 (y)

  3           6 LOAD_FAST                0 (x)
              9 LOAD_FAST                1 (y)
             12 BINARY_MULTIPLY     
             13 RETURN_VALUE

Como pueden ver es mucha mas simple, la primera columna (de izquierda a derecha) es la linea en donde esta esa parte del código la siguiente columna es el offset del bytecode (‘|\x00\x00|\x00\x00\x14S’), las ultimas dos solo estan presentes si la instruccion tiene argumentos, la cuarta columna representa el indice de donde se encuentra este argumento en co_constants o co_varnames y la ultima es basicamente el resultado que nos arroja co_constants o co_varnames, ejemplo:


>>> foo.func_code.co_consts[1]
3
>>> foo.func_code.co_varnames[1]
'y'

Aunque LOAD_CONST, STORE_FAST, etc., son mejor que ‘d\x01\x00}\x01\x00|\x00\x00|\x01\x00\x14S’ tal vez quieras un poco mas de informacion acerca de estas instrucciones, esto lo puedes encontrar aqui: https://docs.python.org/2/library/dis.html#python-bytecode-instructions

Y para que nos sirve esto? bueno, nos sirve para entender mas a fondo Python, por ejemplo cuando estaba investigando sobre bytecode me encontré un ejemplo de como elif y else if en bytecode es lo mismo:


def flat(num):
    if num % 3 == 0:
        print "Fizz"
    elif num % 5 == 0:
        print "Buzz"
    else:
        print num

def nested(num):
    if num % 3 == 0:
        print "Fizz"
    else:
        if num % 5 == 0:
            print "Buzz"
        else:
            print num

dis.dis(flat)
  2           0 LOAD_FAST                0 (num)
              3 LOAD_CONST               1 (3)
              6 BINARY_MODULO       
              7 LOAD_CONST               2 (0)
             10 COMPARE_OP               2 (==)
             13 POP_JUMP_IF_FALSE       24

  3          16 LOAD_CONST               3 ('Fizz')
             19 PRINT_ITEM          
             20 PRINT_NEWLINE       
             21 JUMP_FORWARD            29 (to 53)

  4     >>   24 LOAD_FAST                0 (num)
             27 LOAD_CONST               4 (5)
             30 BINARY_MODULO       
             31 LOAD_CONST               2 (0)
             34 COMPARE_OP               2 (==)
             37 POP_JUMP_IF_FALSE       48

  5          40 LOAD_CONST               5 ('Buzz')
             43 PRINT_ITEM          
             44 PRINT_NEWLINE       
             45 JUMP_FORWARD             5 (to 53)

  7     >>   48 LOAD_FAST                0 (num)
             51 PRINT_ITEM          
             52 PRINT_NEWLINE       
        >>   53 LOAD_CONST               0 (None)
             56 RETURN_VALUE        
dis.dis(nested)
 10           0 LOAD_FAST                0 (num)
              3 LOAD_CONST               1 (3)
              6 BINARY_MODULO       
              7 LOAD_CONST               2 (0)
             10 COMPARE_OP               2 (==)
             13 POP_JUMP_IF_FALSE       24

 11          16 LOAD_CONST               3 ('Fizz')
             19 PRINT_ITEM          
             20 PRINT_NEWLINE       
             21 JUMP_FORWARD            29 (to 53)

 13     >>   24 LOAD_FAST                0 (num)
             27 LOAD_CONST               4 (5)
             30 BINARY_MODULO       
             31 LOAD_CONST               2 (0)
             34 COMPARE_OP               2 (==)
             37 POP_JUMP_IF_FALSE       48

 14          40 LOAD_CONST               5 ('Buzz')
             43 PRINT_ITEM          
             44 PRINT_NEWLINE       
             45 JUMP_FORWARD             5 (to 53)

 16     >>   48 LOAD_FAST                0 (num)
             51 PRINT_ITEM          
             52 PRINT_NEWLINE       
        >>   53 LOAD_CONST               0 (None)
             56 RETURN_VALUE 

También me encontré otro ejemplo de como optimizar código y prevenir stack overflow modificando el bytecode:

http://www.slideshare.net/lnikolaeva/tailbytes-pygotham

Hay muchas razones por la cual saber este tipo de cosas pero para mi solo es por diversión, espero y hayan aprendido algo, cualquier duda, corrección o aclaración me pueden dejar un comentario 🙂

It's only fair to share...Share on Facebook4Tweet about this on TwitterShare on Google+1Share on LinkedIn3Share on Reddit0Pin on Pinterest0Email this to someone

Leave a Reply

Your email address will not be published. Required fields are marked *

Comment moderation is enabled. Your comment may take some time to appear.