Comment Pony (ORM) fait ses tours?

Pony ORM permet de convertir une expression de générateur en SQL. Exemple:

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

Je sais que Python a de magnifiques fonctions intégrées d’introspection et de métaprogrammation, mais comment cette bibliothèque est capable de traduire l’expression du générateur sans prétraitement? Cela ressemble à de la magie.

[mettre à jour]

Blender a écrit:

Voici le fichier que vous recherchez. Il semble reconstruire le générateur en utilisant une magie d’introspection. Je ne suis pas sûr qu’il supporte 100% de la syntaxe de Python, mais c’est plutôt cool. – Mixeur

Je pensais qu’ils exploraient certaines fonctionnalités du protocole d’expression de générateur, mais en regardant ce fichier, et en voyant le module ast impliqué … Non, ils n’inspectent pas la source du programme à la volée, n’est-ce pas? Hallucinant …

@BrenBarn: Si j’essaie d’appeler le générateur en dehors de l’appel de la fonction select , le résultat est le suivant:

 >>> x = (p for p in Person if p.age > 20) >>> x.next() Traceback (most recent call last): File "", line 1, in  File "", line 1, in  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 >>> 

On dirait qu’ils font des incantations plus mystérieuses, comme inspecter l’appel de la fonction select et traiter à la volée l’arbre de syntaxe de la syntaxe abstraite Python.

Je voudrais encore voir quelqu’un l’expliquer, la source est bien au-delà de mon niveau de magie.

L’auteur de poney ORM est ici.

Pony traduit le générateur Python en requête SQL en trois étapes:

  1. Décompilation du bytecode du générateur et du générateur de reconstruction AST (arbre de syntaxe abstraite)
  2. Traduction de Python AST en “SQL abstrait” – représentation universelle à base de liste d’une requête SQL
  3. Conversion de la représentation SQL abstraite en dialecte SQL spécifique à la firebase database

La partie la plus complexe est la deuxième étape, où Pony doit comprendre la “signification” des expressions Python. Il semble que la première étape vous intéresse le plus, alors laissez-moi vous expliquer comment fonctionne la décompilation.

Considérons cette requête:

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

Qui sera traduit dans le SQL suivant:

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

Et ci-dessous est le résultat de cette requête qui sera imprimé:

 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 fonction select() accepte un générateur python comme argument, puis parsing son bytecode. Nous pouvons obtenir des instructions de bytecode de ce générateur en utilisant le module standard 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 ORM a la fonction decomstack() dans le module pony.orm.decompiling qui peut restaurer un AST depuis le bytecode:

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

Ici, nous pouvons voir la représentation textuelle des nœuds AST:

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

Voyons maintenant comment fonctionne la fonction decomstack() .

La fonction decomstack() crée un object decomstack() , qui implémente le motif Visitor. L’instance de décompilateur obtient des instructions de code octet un par un. Pour chaque instruction, l’object de décomstackr appelle sa propre méthode. Le nom de cette méthode est égal au nom de l’instruction de bytecode en cours.

Lorsque Python calcule une expression, il utilise la stack, qui stocke un résultat intermédiaire de calcul. L’object de décompilateur possède également sa propre stack, mais cette stack ne stocke pas le résultat du calcul de l’expression, mais le nœud AST pour l’expression.

Lorsque la méthode de décompilateur pour la prochaine instruction de bytecode est appelée, elle prend les nœuds AST de la stack, les combine dans un nouveau nœud AST, puis place ce nœud en haut de la stack.

Par exemple, voyons comment la sous-expression c.country == 'USA' est calculée. Le fragment correspondant de bytecode est:

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

Ainsi, l’object décompilateur effectue les opérations suivantes:

  1. Appelle decomstackr.LOAD_FAST('c') . Cette méthode place le nœud Name('c') en haut de la stack de décompilateurs.
  2. Appelle decomstackr.LOAD_ATTR('country') . Cette méthode prend le noeud Name('c') de la stack, crée le Geattr(Name('c'), 'country') et le place en haut de la stack.
  3. Appelle decomstackr.LOAD_CONST('USA') . Cette méthode place le noeud Const('USA') au-dessus de la stack.
  4. Appelle decomstackr.COMPARE_OP('==') . Cette méthode prend deux nœuds (Getattr et Const) de la stack, puis place Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) sur le haut de la stack.

Une fois toutes les instructions de bytecode traitées, la stack de décompilateurs contient un seul nœud AST qui correspond à l’expression entière du générateur.

Puisque Pony ORM doit décomstackr uniquement les générateurs et les lambdas, ce n’est pas très complexe, car le stream d’instructions d’un générateur est relativement simple: il ne s’agit que d’un tas de boucles nestedes.

Actuellement, Pony ORM couvre l’ensemble des instructions du générateur, sauf deux choses:

  1. Inline if expressions: a if b else c
  2. Comparaisons composées: a < b < c

Si Pony rencontre une telle expression, il déclenche l'exception NotImplementedError . Mais même dans ce cas, vous pouvez le faire fonctionner en transmettant l'expression du générateur sous forme de chaîne. Lorsque vous passez un générateur sous forme de chaîne, Pony n'utilise pas le module de décompilation. Au lieu de cela, il obtient l'AST en utilisant la fonction standard comstackr.parse Python.

J'espère que ça répond à ta question.