Come Pony (ORM) fa i suoi trucchi?


111

Pony ORM fa il bel trucco di convertire un'espressione del generatore in SQL. Esempio:

>>> 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 che Python ha una meravigliosa introspezione e metaprogrammazione incorporata, ma come questa libreria è in grado di tradurre l'espressione del generatore senza pre-elaborazione? Sembra una magia.

[aggiornare]

Blender ha scritto:

Ecco il file che stai cercando. Sembra ricostruire il generatore usando qualche magia di introspezione. Non sono sicuro che supporti il ​​100% della sintassi di Python, ma questo è piuttosto interessante. - Miscelatore

Stavo pensando che stessero esplorando alcune funzionalità del protocollo di espressione del generatore, ma guardando questo file e vedendo il astmodulo coinvolto ... No, non stanno ispezionando il sorgente del programma al volo, vero? Mind-blowing ...

@BrenBarn: se provo a chiamare il generatore al di fuori della selectchiamata di 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 stiano facendo incantesimi più arcani come ispezionare la selectchiamata di funzione ed elaborare al volo l'albero grammaticale della sintassi astratta di Python.

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


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

3
Ecco il file che stai cercando. Sembra ricostruire il generatore usando qualche magia di introspezione. Non sono sicuro che supporti il ​​100% della sintassi di Python, ma questo è piuttosto interessante.
Blender

1
@ Blender: ho visto questo tipo di trucco in LISP - fare questa acrobazia in Python è semplicemente incredibile!
Paulo Scardine

Risposte:


209

L'autore di Pony ORM è qui.

Pony traduce il generatore Python in query SQL in tre passaggi:

  1. Decompilazione del bytecode del generatore e ricostruzione del generatore AST (albero di sintassi astratto)
  2. Traduzione di Python AST in "SQL astratto" - rappresentazione universale basata su elenchi di una query SQL
  3. Conversione della rappresentazione SQL astratta in un dialetto SQL specifico dipendente dal database

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

Che verrà tradotto nel seguente SQL:

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

E di seguito è il risultato di questa query che verrà stampato:

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()funzione accetta un generatore Python come argomento e quindi analizza il suo bytecode. Possiamo ottenere istruzioni bytecode di questo generatore usando il dismodulo standard 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 ha la funzione decompile()all'interno del modulo pony.orm.decompilingche 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 decompile()funziona la funzione.

La decompile()funzione crea un Decompileroggetto, che implementa il pattern Visitor. L'istanza del decompilatore riceve le istruzioni bytecode una alla volta. 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 del 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 viene chiamato il metodo del decompilatore per la successiva istruzione bytecode, prende i nodi AST dallo stack, li combina in un nuovo nodo AST e quindi mette questo nodo in cima allo stack.

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

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

Quindi, l'oggetto decompilatore fa quanto segue:

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

Dopo che tutte le istruzioni bytecode sono state elaborate, lo stack del decompilatore contiene un singolo nodo AST che corrisponde all'intera espressione del generatore.

Poiché Pony ORM ha bisogno di decompilare solo generatori e lambda, questo non è così complesso, perché il flusso di istruzioni per un generatore è relativamente semplice: è solo un mucchio di loop annidati.

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

  1. Inline if espressioni: a if b else c
  2. Confronti composti: a < b < c

Se Pony incontra tale espressione solleva l' NotImplementedErroreccezione. Ma anche in questo caso puoi farlo funzionare passando l'espressione del generatore come stringa. Quando si passa un generatore come stringa, Pony non usa il modulo di decompilazione. Invece ottiene l'AST utilizzando la compiler.parsefunzione Python standard .

Spero che questo risponda alla tua domanda.


26
Molto performante: (1) la decompilazione del bytecode è molto veloce. (2) Poiché ogni query ha un oggetto codice corrispondente, questo oggetto codice può essere utilizzato come chiave cache. Per questo motivo, Pony ORM traduce ogni query solo una volta, mentre Django e SQLAlchemy devono tradurre la stessa query ancora e ancora. (3) Poiché Pony ORM utilizza il pattern IdentityMap, memorizza nella cache i risultati delle query all'interno della stessa transazione. C'è un post (in russo) in cui l'autore afferma che Pony ORM si è rivelato 1,5-3 volte più veloce di Django e SQLAlchemy anche senza la memorizzazione nella cache dei risultati della query: habrahabr.ru/post/188842
Alexander Kozlovsky

3
È compatibile con il compilatore JIT pypy?
Mzzl

2
Non l'ho provato, ma alcuni commentatori di Reddit dicono che è compatibile: tinyurl.com/ponyorm-pypy
Alexander Kozlovsky,

9
SQLAlchemy ha la memorizzazione nella cache delle query e ORM fa un uso estensivo di questa funzionalità. Non è attivo per impostazione predefinita perché è vero che non abbiamo una funzionalità in atto per collegare la costruzione di un'espressione SQL alla posizione nel codice sorgente che è dichiarata, che è ciò che l'oggetto codice ti sta realmente dando. Potremmo usare l'ispezione dello stack frame per ottenere lo stesso risultato, ma è solo un po 'troppo complicato per i miei gusti. La generazione di SQL è comunque l'area di prestazioni meno critica; il recupero delle righe e delle modifiche contabili è.
zzzeek

2
@ randomsurfer_123 probabilmente no, abbiamo solo bisogno di un po 'di tempo per implementarlo (forse una settimana), e ci sono altre attività che sono più importanti per noi.
Alexander Kozlovsky
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.