Wie macht Pony (ORM) seine Tricks?

111

Pony ORM macht den netten Trick, einen Generatorausdruck in SQL zu konvertieren. Beispiel:

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

Ich weiß, dass in Python eine wunderbare Selbstbeobachtung und Metaprogrammierung integriert ist, aber wie kann diese Bibliothek den Generatorausdruck ohne Vorverarbeitung übersetzen? Es sieht aus wie Magie.

[aktualisieren]

Blender schrieb:

Hier ist die Datei , nach der Sie suchen. Es scheint den Generator mit Hilfe einer Selbstbeobachtungs-Zauberei zu rekonstruieren. Ich bin nicht sicher, ob es 100% der Python-Syntax unterstützt, aber das ist ziemlich cool. - Mixer

Ich dachte, sie würden einige Funktionen des Generator-Ausdrucksprotokolls untersuchen, aber diese Datei durchsuchen und das betreffende astModul sehen ... Nein, sie überprüfen die Programmquelle nicht im laufenden Betrieb, oder? Unglaublich...

@BrenBarn: Wenn ich versuche, den Generator außerhalb des selectFunktionsaufrufs aufzurufen, ist das Ergebnis:

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

Es scheint, als würden sie mehr arkane Beschwörungsformeln ausführen, z. B. den selectFunktionsaufruf überprüfen und den Grammatikbaum der abstrakten Python-Syntax im laufenden Betrieb verarbeiten.

Ich würde immer noch gerne jemanden sehen, der es erklärt, die Quelle liegt weit über meinem Zauberer-Level.

Paulo Scardine
quelle
Vermutlich das pObjekt ein Objekt eines Typs von Pony implementiert , dass schaut, was Methoden / Eigenschaften auf sie zugegriffen wird ( zum Beispiel name, startswith) und wandelt sie in SQL.
BrenBarn
3
Hier ist die Datei, nach der Sie suchen. Es scheint, als würde der Generator mithilfe einer Introspektions-Zauberei rekonstruiert. Ich bin nicht sicher, ob es 100% der Python-Syntax unterstützt, aber das ist ziemlich cool.
Blender
1
@Blender: Ich habe diese Art von Trick in LISP gesehen - diesen Stunt in Python zu ziehen ist einfach nur krank!
Paulo Scardine

Antworten:

209

Pony ORM Autor ist hier.

Pony übersetzt den Python-Generator in drei Schritten in eine SQL-Abfrage:

  1. Dekompilierung des Generator-Bytecodes und Neuerstellung des Generator-AST (abstrakter Syntaxbaum)
  2. Übersetzung von Python AST in "abstract SQL" - universelle listenbasierte Darstellung einer SQL-Abfrage
  3. Konvertieren der abstrakten SQL-Darstellung in einen bestimmten datenbankabhängigen SQL-Dialekt

Der komplexeste Teil ist der zweite Schritt, in dem Pony die "Bedeutung" von Python-Ausdrücken verstehen muss. Anscheinend interessiert Sie der erste Schritt am meisten. Lassen Sie mich erklären, wie das Dekompilieren funktioniert.

Betrachten wir diese Abfrage:

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

Welches wird in die folgende SQL übersetzt:

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

Und unten ist das Ergebnis dieser Abfrage, die ausgedruckt wird:

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

Die select()Funktion akzeptiert einen Python-Generator als Argument und analysiert dann seinen Bytecode. Wir können Bytecode-Anweisungen dieses Generators mit dem Standard-Python- disModul erhalten:

>>> 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 hat die Funktion decompile()innerhalb des Moduls pony.orm.decompiling, die einen AST aus dem Bytecode wiederherstellen kann:

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

Hier sehen wir die Textdarstellung der AST-Knoten:

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

Mal sehen, wie die decompile()Funktion funktioniert.

Die decompile()Funktion erstellt ein DecompilerObjekt, das das Besuchermuster implementiert. Die Dekompilerinstanz erhält nacheinander Bytecode-Anweisungen. Für jede Anweisung ruft das Dekompilerobjekt eine eigene Methode auf. Der Name dieser Methode entspricht dem Namen der aktuellen Bytecode-Anweisung.

Wenn Python einen Ausdruck berechnet, wird ein Stapel verwendet, in dem ein Zwischenergebnis der Berechnung gespeichert wird. Das Dekompilerobjekt hat auch einen eigenen Stapel, aber dieser Stapel speichert nicht das Ergebnis der Ausdrucksberechnung, sondern den AST-Knoten für den Ausdruck.

Wenn die Dekompilierungsmethode für den nächsten Bytecode-Befehl aufgerufen wird, werden AST-Knoten vom Stapel genommen, zu einem neuen AST-Knoten kombiniert und dieser Knoten dann oben auf dem Stapel platziert.

Lassen Sie uns zum Beispiel sehen, wie der Unterausdruck c.country == 'USA'berechnet wird. Das entsprechende Bytecode-Fragment lautet:

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

Das Dekompilerobjekt führt also Folgendes aus:

  1. Anrufe decompiler.LOAD_FAST('c'). Diese Methode platziert den Name('c')Knoten oben auf dem Dekompiler-Stapel.
  2. Anrufe decompiler.LOAD_ATTR('country'). Diese Methode nimmt den Name('c')Knoten vom Stapel, erstellt den Geattr(Name('c'), 'country')Knoten und legt ihn oben auf den Stapel.
  3. Anrufe decompiler.LOAD_CONST('USA'). Diese Methode legt den Const('USA')Knoten oben auf den Stapel.
  4. Anrufe decompiler.COMPARE_OP('=='). Diese Methode nimmt zwei Knoten (Getattr und Const) aus dem Stapel und legt sie dann Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) oben auf den Stapel.

Nachdem alle Bytecode-Anweisungen verarbeitet wurden, enthält der Dekompiler-Stapel einen einzelnen AST-Knoten, der dem gesamten Generatorausdruck entspricht.

Da Pony ORM nur Generatoren und Lambdas dekompilieren muss, ist dies nicht so komplex, da der Befehlsfluss für einen Generator relativ einfach ist - es handelt sich nur um eine Reihe verschachtelter Schleifen.

Derzeit deckt Pony ORM den gesamten Generatorbefehlssatz ab, mit Ausnahme von zwei Dingen:

  1. Inline wenn Ausdrücke: a if b else c
  2. Zusammengesetzte Vergleiche: a < b < c

Wenn Pony auf einen solchen Ausdruck stößt, wird die NotImplementedErrorAusnahme ausgelöst . Aber auch in diesem Fall können Sie es zum Laufen bringen, indem Sie den Generatorausdruck als Zeichenfolge übergeben. Wenn Sie einen Generator als String übergeben, verwendet Pony das Dekompilierermodul nicht. Stattdessen wird der AST mit der Standard-Python- compiler.parseFunktion abgerufen .

Hoffe das beantwortet deine Frage.

Alexander Kozlovsky
quelle
26
Sehr performant: (1) Die Dekompilierung von Bytecodes ist sehr schnell. (2) Da jede Abfrage ein entsprechendes Codeobjekt hat, kann dieses Codeobjekt als Cache-Schlüssel verwendet werden. Aus diesem Grund übersetzt Pony ORM jede Abfrage nur einmal, während Django und SQLAlchemy dieselbe Abfrage immer wieder übersetzen müssen. (3) Da Pony ORM das IdentityMap-Muster verwendet, werden Abfrageergebnisse innerhalb derselben Transaktion zwischengespeichert. Es gibt einen Beitrag (auf Russisch), in dem der Autor angibt, dass Pony ORM auch ohne Zwischenspeicherung der Abfrageergebnisse
Alexander Kozlovsky
3
Ist dies mit dem Pypy JIT Compiler kompatibel?
Mzzl
2
Ich habe es nicht getestet, aber ein Reddit-Kommentator sagt, es sei kompatibel: tinyurl.com/ponyorm-pypy
Alexander Kozlovsky
9
SQLAlchemy verfügt über Abfrage-Caching, und das ORM nutzt diese Funktion in großem Umfang. Es ist nicht standardmäßig aktiviert, da es wahr ist, dass wir keine Funktion haben, um die Konstruktion eines SQL-Ausdrucks mit der Position im Quellcode zu verknüpfen, die deklariert ist, was das Codeobjekt Ihnen wirklich gibt. Wir könnten die Stapelrahmeninspektion verwenden, um das gleiche Ergebnis zu erzielen, aber das ist für meinen Geschmack etwas zu hackig. Die Generierung von SQL ist in jedem Fall der am wenigsten kritische Leistungsbereich. Abrufen von Zeilen und Änderungen in der Buchhaltung ist.
Zzzeek
2
@ randomsurfer_123 wahrscheinlich nicht, wir brauchen nur etwas Zeit, um es zu implementieren (vielleicht eine Woche), und es gibt andere Aufgaben, die für uns wichtiger sind.
Alexander Kozlovsky