sábado, julio 25, 2009

generando funciones dinamicamente (a.k.a generando bytecode a mano)

Este desafio estuvo bueno, alguien en la lista de emesene dijo que para hacer la API de dbus exponiendo los eventos del protocolo (que tienen numero de argumentos variables) necesitaba generar funciones con cantidad de argumentos variables pero que la funcion tenga ese numero fijo, esto significa que si tengo que generar una funcion que recibe 4 argumentos tiene que ser:

def fun(arg0, arg1, arg2, arg3): pass

y no

def fun(*args): pass

ya que la API de dbus usa inspect para determinar el numero de argumentos posicionales

me tomo varias horas sacarlo, buscaba y buscaba y cada vez iba mas a bajo nivel. Hasta que llegue al mas bajo nivel que se puede llegar... generar bytecodes :)

y bue, pelee con eso un buen rato ya que si bien alguna vez jugue haciendo maquinas virtuales de juguete esta era la primera vez contra una en serio.

el resultado es el siguiente:


import dis
import types

def gen(num):
op = dis.opmap.__getitem__
ops = [op('LOAD_GLOBAL'), 0, 0]

for argnum in range(num):
ops.append(op('LOAD_FAST'))
ops.append(argnum)
ops.append(0)

ops.append(op('CALL_FUNCTION'))
ops.append(num)
ops.append(0)
ops.append(op('RETURN_VALUE'))

code_str = ''.join(chr(x) for x in ops)
return code_str

def gen_code(num, global_vars, name='func', filename='magic.py'):
code = gen(num)
varnames = ['arg' + str(x) for x in range(num)]
names = global_vars + varnames

names = tuple(names)
varnames = tuple(varnames)

return types.CodeType(num, num, num, 0, code, (), names, varnames, filename, name, 1, '')

def gen_fun(num, name, func):
code = gen_code(num, [func.__name__], name)
return types.FunctionType(code,{func.__name__: func})

if __name__ == '__main__':
def printer(*args):
print args

f4 = gen_fun(4, 'f4', printer)
dis.dis(f4.func_code)
f4(1,2,3,4)

f1 = gen_fun(1, 'f1', printer)
dis.dis(f1.func_code)
f1('only one arg')

try:
f1(1, 2)
except TypeError:
print 'ok, ok, only one argument'


lo que hace es generar una funcion que recibe N parametros y que lo unico que hace es llamar a otra funcion pasandole esos parametros, lo cual seria algo como:

def fun1(*args): print args

def fun_que_recibe_4_parametros(a,b,c,d): fun1(a,b,c,d)

con lo que si escribimos nuestra funcion en fun1 y creamos las funciones que reciben los distintos parametros con un for tenemos lo que necesitamos :)

la salida de la ejecusion de lo de arriba es:

1 0 LOAD_GLOBAL 0 (printer)
3 LOAD_FAST 0 (arg0)
6 LOAD_FAST 1 (arg1)
9 LOAD_FAST 2 (arg2)
12 LOAD_FAST 3 (arg3)
15 CALL_FUNCTION 4
18 RETURN_VALUE

(1, 2, 3, 4)

1 0 LOAD_GLOBAL 0 (printer)
3 LOAD_FAST 0 (arg0)
6 CALL_FUNCTION 1
9 RETURN_VALUE

('only one arg',)
ok, ok, only one argument


fuentes de inspiracion:

http://pyref.infogami.com/type-code
http://docs.python.org/library/dis.html
http://docs.python.org/library/types
http://docs.python.org/reference/datamodel.html#the-standard-type-hierarchy (la parte de callable types)

5 comentarios:

Juanjo Conti dijo...

Se podría haber usado decoradores para lograr lo mismo?

Juanjo Conti dijo...

Cómo así:
http://codepad.org/J1nFBej9.

(comentame algo, disculpá si no era la idea, pero escribí esto volando antes de irme a una fiesta).

Jeffry O'Nassto dijo...

http://us.php.net/manual/en/function.func-get-args.php

http://us.php.net/manual/en/function.create-function.php

hacelo en php buacho

luismarianoguerra dijo...

@Juanjo: no es lo mismo, lo que explico en el post es que necesito que la funcion tenga exactamente ese numero de argumentos porque el modulo de dbus usa inspect para saber cuantos argumentos recibe la funcion, y si no es la cantidad especificada en el decorador no te deja usar la funcion.

Por lo que no puedo usar *args, lo podes ver bien aca http://cgit.freedesktop.org/dbus/dbus-python/tree/dbus/decorators.py#n327

@Nassto: eso casi como eval (feo) y ademas eso se puede hacer de una forma mucho mas simple en python :D

Marcelo dijo...

¡Groso!

¿Seguro que con type() no lo podías hacer, no? (descarto lambda también...).

Interesante lo del types.FunctionType(code, ...) :-)

Salutes

Seguidores

Archivo del Blog