python orm metaprogramming dsl ponyorm

python - ¿Cómo Pony(ORM) hace sus trucos?



pony orm get by id (1)

El autor de Pony ORM está aquí.

Pony traduce el generador de Python a la consulta SQL en tres pasos:

  1. Descompilación del generador de códigos de bytes y generador de reconstrucción AST (árbol de sintaxis abstracta)
  2. Traducción de Python AST en "SQL abstracto" - representación universal basada en listas de una consulta SQL
  3. Conversión de representación abstracta de SQL en un dialecto SQL dependiente de una base de datos específica

La parte más compleja es el segundo paso, donde Pony debe entender el "significado" de las expresiones de Python. Parece que estás más interesado en el primer paso, así que déjame explicarte cómo funciona la descompilación.

Consideremos esta consulta:

>>> from pony.orm.examples.estore import * >>> select(c for c in Customer if c.country == ''USA'').show()

Que se traducirá al siguiente SQL:

SELECT "c"."id", "c"."email", "c"."password", "c"."name", "c"."country", "c"."address" FROM "Customer" "c" WHERE "c"."country" = ''USA''

A continuación se muestra el resultado de esta consulta que se imprimirá:

id|email |password|name |country|address --+-------------------+--------+--------------+-------+--------- 1 |[email protected] |*** |John Smith |USA |address 1 2 |[email protected]|*** |Matthew Reed |USA |address 2 4 |[email protected]|*** |Rebecca Lawson|USA |address 4

La función select() acepta un generador de python como argumento, y luego analiza su bytecode. Podemos obtener instrucciones de bytecode de este generador usando el módulo python dis estándar:

>>> gen = (c for c in Customer if c.country == ''USA'') >>> import dis >>> dis.dis(gen.gi_frame.f_code) 1 0 LOAD_FAST 0 (.0) >> 3 FOR_ITER 26 (to 32) 6 STORE_FAST 1 (c) 9 LOAD_FAST 1 (c) 12 LOAD_ATTR 0 (country) 15 LOAD_CONST 0 (''USA'') 18 COMPARE_OP 2 (==) 21 POP_JUMP_IF_FALSE 3 24 LOAD_FAST 1 (c) 27 YIELD_VALUE 28 POP_TOP 29 JUMP_ABSOLUTE 3 >> 32 LOAD_CONST 1 (None) 35 RETURN_VALUE

Pony ORM tiene la función decompile() dentro del módulo pony.orm.decompiling que puede restaurar un AST desde el bytecode:

>>> from pony.orm.decompiling import decompile >>> ast, external_names = decompile(gen)

Aquí, podemos ver la representación textual de los nodos AST:

>>> ast GenExpr(GenExprInner(Name(''c''), [GenExprFor(AssName(''c'', ''OP_ASSIGN''), Name(''.0''), [GenExprIf(Compare(Getattr(Name(''c''), ''country''), [(''=='', Const(''USA''))]))])]))

Veamos ahora cómo funciona la función decompile() .

La función decompile() crea un objeto Decompiler , que implementa el patrón Visitor. La instancia del descompilador recibe instrucciones bytecode una a una. Para cada instrucción, el objeto descompilador llama a su propio método. El nombre de este método es igual al nombre de la instrucción de bytecode actual.

Cuando Python calcula una expresión, usa la pila, que almacena un resultado intermedio de cálculo. El objeto descompilador también tiene su propia pila, pero esta pila no almacena el resultado del cálculo de la expresión, sino el nodo AST para la expresión.

Cuando se llama al método decompiler para la siguiente instrucción de bytecode, toma los nodos AST de la pila, los combina en un nuevo nodo AST y luego coloca este nodo en la parte superior de la pila.

Por ejemplo, veamos cómo se calcula la subexpresión c.country == ''USA'' . El fragmento de código de bytes correspondiente es:

9 LOAD_FAST 1 (c) 12 LOAD_ATTR 0 (country) 15 LOAD_CONST 0 (''USA'') 18 COMPARE_OP 2 (==)

Entonces, el objeto descompilador hace lo siguiente:

  1. Llamadas decompiler.LOAD_FAST(''c'') . Este método coloca el nodo Name(''c'') en la parte superior de la pila del descompilador.
  2. Llamadas decompiler.LOAD_ATTR(''country'') . Este método toma el nodo Name(''c'') de la pila, crea el Geattr(Name(''c''), ''country'') y lo coloca en la parte superior de la pila.
  3. Llamadas decompiler.LOAD_CONST(''USA'') . Este método coloca el nodo Const(''USA'') en la parte superior de la pila.
  4. Llamadas decompiler.COMPARE_OP(''=='') . Este método toma dos nodos (Getattr y Const) de la pila y luego Compare(Getattr(Name(''c''), ''country''), [(''=='', Const(''USA''))]) en la parte superior de la pila.

Después de que se procesen todas las instrucciones de bytecode, la pila de decompiladores contiene un único nodo AST que corresponde a la expresión del generador completo.

Como Pony ORM necesita descompilar generadores y lambdas solamente, esto no es tan complejo, porque el flujo de instrucciones para un generador es relativamente sencillo: es solo un conjunto de bucles anidados.

Actualmente Pony ORM cubre todo el conjunto de instrucciones del generador excepto dos cosas:

  1. Inline if expressions: a if b else c
  2. Comparaciones compuestas: a < b < c

Si Pony encuentra dicha expresión, aumenta la excepción NotImplementedError . Pero incluso en este caso puede hacer que funcione pasando la expresión del generador como una cadena. Cuando pasa un generador como una cadena, Pony no usa el módulo decompilador. En su lugar, obtiene el AST utilizando la función estándar compiler.parse Python.

Espero que esto responda a su pregunta.

Pony ORM hace el bonito truco de convertir una expresión de generador en SQL. Ejemplo:

>>> select(p for p in Person if p.name.startswith(''Paul'')) .order_by(Person.name)[:2] SELECT "p"."id", "p"."name", "p"."age" FROM "Person" "p" WHERE "p"."name" LIKE "Paul%" ORDER BY "p"."name" LIMIT 2 [Person[3], Person[1]] >>>

Sé que Python tiene una introspección maravillosa y metaprogramación incorporada, pero ¿cómo esta biblioteca puede traducir la expresión del generador sin preprocesamiento? Parece magia.

[actualizar]

Blender escribió:

Aquí está el archivo que buscas. Parece reconstruir el generador usando algo de magia de introspección. No estoy seguro de si admite el 100% de la sintaxis de Python, pero esto es genial. - Licuadora

Estaba pensando que estaban explorando alguna característica del protocolo de expresión del generador, pero mirando este archivo, y viendo el módulo ast involucrado ... No, no están inspeccionando la fuente del programa sobre la marcha, ¿o sí? Alucinante ...

@BrenBarn: Si trato de llamar al generador fuera de la llamada a la función de select , el resultado es:

>>> x = (p for p in Person if p.age > 20) >>> x.next() Traceback (most recent call last): File "<interactive input>", line 1, in <module> File "<interactive input>", line 1, in <genexpr> File "C:/Python27/lib/site-packages/pony/orm/core.py", line 1822, in next % self.entity.__name__) File "C:/Python27/lib/site-packages/pony/utils.py", line 92, in throw raise exc TypeError: Use select(...) function or Person.select(...) method for iteration >>>

Parece que están haciendo encantamientos más arcanos como inspeccionar la llamada de función de select y procesar el árbol de gramática de sintaxis abstracta de Python sobre la marcha.

Todavía me gustaría ver a alguien que lo explique, la fuente está mucho más allá de mi nivel de magia.