2013-04-20 17 views
99

Pony ORM fa il bello trucco di convertire un'espressione di generatore in SQL. Esempio:Come Pony (ORM) fa i suoi trucchi?

>>> 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]] 
>>> 

so Python ha meraviglioso introspezione e metaprogrammazione incorporato, ma come questa libreria è in grado di tradurre l'espressione generatore, senza pre-elaborazione? Sembra magia.

[update]

Blender ha scritto:

Here is the file che siete dopo. Sembra ricostruire il generatore usando un po 'di magia introspettiva. Non sono sicuro che supporti il ​​100% della sintassi di Python, ma è piuttosto interessante. - Blender

Stavo pensando che stavano esplorando qualche caratteristica dal protocollo generatore di espressione, ma guardando questo file, e vedere il modulo ast coinvolti ... No, non stanno controllando la fonte di programma al volo, sono essi? Mind-blowing ...

@BrenBarn: Se provo a chiamare il generatore di fuori della chiamata select funzione, il risultato è:

>>> 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 
>>> 

sembra che stanno facendo incantesimi arcani maggiori come ispezionare la funzione select chiama ed elabora l'albero di grammatica della sintassi astratta Python al volo.

Mi piacerebbe ancora vedere qualcuno che lo spiega, la fonte è ben oltre il mio livello di magia.

+0

Presumibilmente l'oggetto 'p' è un oggetto di un tipo implementato da Pony che esamina a quali metodi/proprietà si sta accedendo su di esso (ad esempio,' name', 'startswith') e li converte in SQL. – BrenBarn

+2

[Qui] (https://github.com/ponyorm/pony/blob/orm/pony/orm/decompiling.py#L52) è il file che stai cercando. Sembra ricostruire il generatore usando un po 'di magia introspettiva. Non sono sicuro che supporti il ​​100% della sintassi di Python, ma è piuttosto interessante. – Blender

+1

@Blender: ho visto questo tipo di trucco in LISP: tirare questa acrobazia in Python è semplicemente un male! –

risposta

183

L'autore di Pony ORM è qui.

Pony traduce generatore di Python in query SQL in tre fasi:

  1. decompilazione del generatore di bytecode e ricostruzione generatore AST (albero di sintassi astratta)
  2. Traduzione di Python AST in "SQL astratto" - universale rappresentazione della lista a base di una query SQL
  3. Conversione rappresentazione astratta SQL in particolare dialetto SQL banca dati-dipendente

La parte più complessa è il secondo passaggio, in cui Pony deve comprendere il "significato" delle espressioni Python. Sembra che tu sia più interessato al primo passaggio, , quindi lascia che ti spieghi come funziona la decompilazione.

Consideriamo questa query:

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

Quale sarà tradotto nelle seguenti SQL:

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

E sotto è il risultato di questa query che verrà stampata:

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 funzione select() accetta un generatore python come argomento, e t gallina analizza il suo bytecode. Siamo in grado di ottenere le istruzioni bytecode di questo generatore utilizzando standard di Python dis modulo:

>>> 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 ha la funzione decompile() entro modulo pony.orm.decompiling che può ripristinare un AST dal bytecode:

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

Qui, possiamo vedere la rappresentazione testuale dei nodi AST:

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

Vediamo ora come funziona la funzione decompile().

La funzione decompile() crea un oggetto Decompiler che implementa il pattern Visitor. L'istanza del decompilatore riceve istruzioni bytecode una per una. Per ogni istruzione l'oggetto decompilatore chiama il proprio metodo. Il nome di questo metodo è uguale al nome dell'istruzione bytecode corrente.

Quando Python calcola un'espressione, utilizza lo stack, che memorizza un risultato intermedio di calcolo . Anche l'oggetto decompilatore ha il proprio stack, ma questo stack non memorizza il risultato del calcolo dell'espressione, ma il nodo AST per l'espressione.

Quando metodo decompilatore per la successiva istruzione bytecode viene chiamato, ci vuole nodi AST dalla pila, combina le in un nuovo nodo AST, e quindi pone questo nodo in cima alla pila.

Ad esempio, vediamo come viene calcolata la sottoespressione c.country == 'USA'. Il frammento corrispondente bytecode è:

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

Quindi, l'oggetto decompilatore esegue le seguenti operazioni:

  1. chiamate decompiler.LOAD_FAST('c'). Questo metodo mette il nodo Name('c') nella parte superiore della pila del decompilatore.
  2. chiamate decompiler.LOAD_ATTR('country'). Questo metodo prende il nodo Name('c') dallo stack, crea il nodo Geattr(Name('c'), 'country') e lo mette in cima allo stack.
  3. Chiamate decompiler.LOAD_CONST('USA'). Questo metodo mette il nodo Const('USA') in cima allo stack.
  4. Chiamate decompiler.COMPARE_OP('=='). Questo metodo prende due nodi (Getattr e Const) dallo stack, e quindi inserisce Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) in cima allo stack.

Dopo che tutte le istruzioni bytecode vengono elaborati, la pila decompiler contiene un singolo nodo AST che corrisponde a tutta generatore di espressione.

Da Pony ORM deve decompilare generatori e lambda solo, questo non è quel complesso, perché il flusso di istruzioni per un generatore è relativamente semplice - è solo un gruppo di cicli nidificati.

Attualmente Pony ORM copre l'intero istruzioni del generatore set, tranne due cose:

  1. Inline se le espressioni: a if b else c
  2. confronti composti: a < b < c

Se Pony incontra tale espressione solleva la NotImplementedError eccezione. Ma anche nel caso puoi farlo funzionare passando l'espressione del generatore come una stringa. Quando si passa un generatore come stringa, Pony non usa il modulo decompilatore. Invece ottiene AST usando la funzione standard Python compiler.parse.

Spero che questo risponda alla tua domanda.

+1

Grande spiegazione e un interessante esempio di "DSL interno" (linguaggio specifico del dominio) implementato in Python. Vedi http://stackoverflow.com/questions/tagged/dsl+python per altri - taggato questa domanda come [dsl]. – RichVel

+0

Fantastico! Grazie per la condivisione. – coleifer

+1

Quanto è performante? – ruipacheco