web-dev-qa-db-fra.com

Comment Pony (ORM) fait ses tours?

Pony ORM fait l'astuce 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 Python a une merveilleuse introspection et métaprogrammation intégrée, mais comment cette bibliothèque est capable de traduire l'expression du générateur sans prétraitement? Cela ressemble à de la magie.

[mise à jour]

Blender a écrit:

Voici le fichier que vous recherchez. Il semble reconstruire le générateur en utilisant un peu de magie d'introspection. Je ne sais pas s'il prend en charge 100% de la syntaxe de Python, mais c'est plutôt cool. - Mixeur

Je pensais qu'ils exploraient une fonctionnalité du protocole d'expression du 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? Époustouflant ...

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

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

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

J'aimerais toujours voir quelqu'un l'expliquer, la source est bien au-delà de mon niveau de sorcellerie.

106
Paulo Scardine

Pony ORM author 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 reconstruction du générateur AST (arbre de syntaxe abstraite)
  2. Traduction de Python AST en "SQL abstrait" - représentation universelle basée sur une liste d'une requête SQL
  3. Conversion d'une représentation SQL abstraite en dialecte SQL spécifique dépendant de la base de données

La partie la plus complexe est la deuxième étape, où Pony doit comprendre la "signification" des expressions Python. Semble que vous êtes le plus intéressé par la première étape, 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 voici le résultat de cette requête qui sera imprimée:

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 analyse 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 decompile() dans le module pony.orm.decompiling Qui peut restaurer un AST à partir du bytecode:

>>> from pony.orm.decompiling import decompile
>>> ast, external_names = decompile(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 decompile().

La fonction decompile() crée un objet Decompiler, qui implémente le modèle Visitor. L'instance de décompilateur obtient les instructions de bytecode une par une. Pour chaque instruction, l'objet décompilateur appelle sa propre méthode. Le nom de cette méthode est égal au nom de l'instruction de bytecode actuelle.

Lorsque Python calcule une expression, il utilise la pile, qui stocke un résultat intermédiaire de calcul. L'objet décompilateur a également sa propre pile, mais cette pile ne stocke pas le résultat du calcul de l'expression, mais le noeud AST pour l'expression.

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

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

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

Ainsi, l'objet décompilateur effectue les opérations suivantes:

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

Une fois toutes les instructions de bytecode traitées, la pile du décompilateur contient un seul noeud AST qui correspond à l'expression entière du générateur.

Étant donné que Pony ORM doit uniquement décompiler les générateurs et les lambdas, ce n'est pas si complexe, car le flux d'instructions pour un générateur est relativement simple - il s'agit simplement d'un tas de boucles imbriquées.

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 de composés: 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 passant 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 le AST en utilisant la fonction standard Python compiler.parse.

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

203