Cómo Pony (Pony) hace sus trucos?


Pony Pony hace el buen truco de convertir una expresión del 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 maravillosa introspección y metaprogramación incorporada, pero ¿cómo esta biblioteca es capaz de traducir la expresión del generador sin preprocesamiento? Parece magia.

[actualización]

Blender escribió:

Aquí está el archivo que buscas. Parece reconstruir el generador usando alguna magia de introspección. No lo soy. seguro que soporta el 100% de la sintaxis de Python, pero esto es bastante genial. - Blender

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, ¿verdad? Alucinante...

@BrenBarn: Si intento llamar al generador fuera de la llamada a la función 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 más arcano encantamientos como inspeccionar la llamada a la función select y procesar el árbol de gramática de sintaxis abstracta de Python sobre la marcha.

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

Author: Alexey Malashkevich, 2013-04-20

1 answers

Pony author author está aquí.

Pony traduce el generador Python en consulta SQL en tres pasos:

  1. Descompilación del bytecode del generador y reconstrucción del generador AST (árbol de sintaxis abstracta)
  2. Traducción de Python AST en "SQL abstracto" universal universal representación basada en listas de una consulta SQL
  3. Conversión de la representación SQL abstracta en dialecto SQL dependiente de la base de datos

La parte más compleja es el segundo paso, donde Pony deber entender el "significado" de las expresiones Python. Parece que eres el más interesado en el primer paso, así que permítanme explicar 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'

Y 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 estándar python dis:

>>> 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 OR 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 de visitante. La instancia del decompiler recibe instrucciones de código de bytes uno por uno. Para cada instrucción el decompiler object llama a su propio método. El nombre de este método es igual al nombre de la instrucción de código de bytes actual.

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

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

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

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

Entonces, el objeto decompiler hace lo siguiente:

  1. Llama decompiler.LOAD_FAST('c'). Este método pone el nodo Name('c') en la parte superior de la pila de descompiladores.
  2. Llama decompiler.LOAD_ATTR('country'). Este método toma el nodo Name('c') de la pila, crea el nodo Geattr(Name('c'), 'country') y lo pone en la parte superior del pila.
  3. Llama decompiler.LOAD_CONST('USA'). Este método pone el nodo Const('USA') en la parte superior de la pila.
  4. Llama decompiler.COMPARE_OP('=='). Este método toma dos nodos (Getattr y Const) de la pila, y luego pone 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 descompiladores contiene un único nodo AST que corresponde a toda la expresión del generador.

Dado que Pony Pony necesita descompilar generadores y lambdas solamente, esto no es tan complejo, porque el flujo de instrucciones para un generador es relativamente sencillo - es sólo un montón de bucles anidados.

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

  1. Expresiones if inline: a if b else c
  2. Comparaciones compuestas: a < b < c

Si Pony encuentra tal expresión, se genera la excepción NotImplementedError. Pero incluso en en este caso, puede hacer que funcione pasando la expresión del generador como una cadena. Cuando se pasa un generator as a string Pony no utiliza el módulo decompiler. En su lugar obtiene el AST usando la función estándar Python compiler.parse.

Espero que esto responda a su pregunta.

 191
Author: Alexander Kozlovsky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-22 17:37:38