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

111

Pony ORM hace el buen truco de convertir una expresión generadora 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 puede traducir la expresión del generador sin preprocesar? Parece magia.

[actualizar]

Blender escribió:

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

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

@BrenBarn: Si intento llamar al generador fuera de la selectllamada a la función, 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 selectllamada a la función y procesar el árbol gramatical de sintaxis abstracta de Python sobre la marcha.

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

Paulo Scardine
fuente
Presumiblemente, el pobjeto es un objeto de un tipo implementado por Pony que mira qué métodos / propiedades se están accediendo en él (por ejemplo name, startswith) y los convierte a SQL.
BrenBarn
3
Aquí está el archivo que busca. Parece reconstruir el generador usando algo de magia de introspección. No estoy seguro de si es compatible con el 100% de la sintaxis de Python, pero esto es bastante bueno.
Blender
1
@Blender: He visto este tipo de truco en LISP: ¡hacer este truco en Python es simplemente enfermizo!
Paulo Scardine

Respuestas:

209

El autor de Pony ORM está aquí.

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

  1. Descompilación del código de bytes del generador y reconstrucción del AST del generador (á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 la representación SQL abstracta en un dialecto SQL específico dependiente de la base de datos

La parte más compleja es el segundo paso, donde Pony debe comprender 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'

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

id|email              |password|name          |country|address  
--+-------------------+--------+--------------+-------+---------
1 |john@example.com   |***     |John Smith    |USA    |address 1
2 |matthew@example.com|***     |Matthew Reed  |USA    |address 2
4 |rebecca@example.com|***     |Rebecca Lawson|USA    |address 4

La select()función acepta un generador de Python como argumento y luego analiza su código de bytes. Podemos obtener instrucciones de código de bytes de este generador usando el dismódulo estándar de Python :

>>> 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.decompilingque puede restaurar un AST desde el código de bytes:

>>> 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 decompile()funciona la función.

La decompile()función crea un Decompilerobjeto, que implementa el patrón Visitor. La instancia del descompilador obtiene instrucciones de código de bytes una por 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 código de bytes actual.

Cuando Python calcula una expresión, usa pila, que almacena un resultado intermedio del 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 de descompilador para la siguiente instrucción de código de bytes, 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 c.country == 'USA'se calcula la subexpresión . 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 Name('c')nodo en la parte superior de la pila del descompilador.
  2. Llamadas decompiler.LOAD_ATTR('country'). Este método toma el Name('c')nodo de la pila, crea el Geattr(Name('c'), 'country')nodo y lo coloca en la parte superior de la pila.
  3. Llamadas decompiler.LOAD_CONST('USA'). Este método coloca el Const('USA')nodo 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 los coloca Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) en la parte superior de la pila.

Después de que se procesan todas las instrucciones de código de bytes, la pila del descompilador contiene un solo nodo AST que corresponde a toda la expresión del generador.

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

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

  1. Expresiones if en línea: a if b else c
  2. Comparaciones compuestas: a < b < c

Si Pony encuentra tal expresión, plantea la NotImplementedErrorexcepción. Pero incluso en este caso, puede hacer que funcione pasando la expresión del generador como una cadena. Cuando pasas un generador como una cadena, Pony no usa el módulo descompilador. En su lugar, obtiene el AST utilizando la compiler.parsefunción estándar de Python .

Espero que esto responda a su pregunta.

Alexander Kozlovsky
fuente
26
Muy eficaz: (1) La descompilación de códigos de bytes es muy rápida. (2) Dado que cada consulta tiene un objeto de código correspondiente, este objeto de código se puede utilizar como clave de caché. Debido a esto, Pony ORM traduce cada consulta solo una vez, mientras que Django y SQLAlchemy tienen que traducir la misma consulta una y otra vez. (3) Como Pony ORM usa el patrón IdentityMap, almacena en caché los resultados de la consulta dentro de la misma transacción. Hay una publicación (en ruso) donde el autor afirma que Pony ORM resultó ser 1.5-3 veces más rápido que Django y SQLAlchemy incluso sin almacenamiento en caché de resultados de consultas: habrahabr.ru/post/188842
Alexander Kozlovsky
3
¿Es esto compatible con el compilador pypy JIT?
Mzzl
2
No lo probé, pero un comentarista de Reddit dice que es compatible: tinyurl.com/ponyorm-pypy
Alexander Kozlovsky
9
SQLAlchemy tiene almacenamiento en caché de consultas y el ORM hace un uso extensivo de esta característica. No está activado de forma predeterminada porque es cierto que no tenemos una función para vincular la construcción de una expresión SQL a la posición en el código fuente que está declarada, que es lo que realmente le está dando el objeto de código. Podríamos usar la inspección del marco de la pila para obtener el mismo resultado, pero eso es demasiado complicado para mi gusto. La generación de SQL es el área de rendimiento menos crítica en cualquier caso; buscar filas y cambios en la contabilidad lo es.
zzzeek
2
@ randomsurfer_123 probablemente no, solo necesitamos algo de tiempo para implementarlo (tal vez una semana), y hay otras tareas que son más importantes para nosotros.
Alexander Kozlovsky