Comment Pony (ORM) fait-il ses tours?

111

Pony ORM fait le bon truc 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 une introspection et une métaprogrammation merveilleuses intégrées, mais comment cette bibliothèque est-elle 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 certaine 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 ast module impliqué ... Non, ils n'inspectent pas la source du programme à la volée, n'est-ce pas? Époustouflant ...

@BrenBarn: Si j'essaye d'appeler le générateur en dehors de l' selectappel de fonction, 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 obscures comme l'inspection de l' selectappel de fonction et le traitement de l'arbre de grammaire de la syntaxe abstraite Python à la volée.

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

Paulo Scardine
la source
On peut supposer que l' pobjet est un objet d'un type mis en œuvre par Pony qui se penche sur les méthodes / propriétés sont accessibles à ce sujet (par exemple name, startswith) et les convertit en SQL.
BrenBarn
3
Voici le fichier que vous recherchez. Il semble reconstruire le générateur en utilisant une certaine magie d'introspection. Je ne sais pas s'il prend en charge 100% de la syntaxe de Python, mais c'est plutôt cool.
Blender
1
@Blender: J'ai vu ce genre de truc dans LISP - tirer ce truc en Python est tout simplement malade!
Paulo Scardine

Réponses:

209

L'auteur de Pony 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 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. Il semble que vous soyez 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()

Ce 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ée:

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()fonction accepte un générateur python comme argument, puis analyse son bytecode. Nous pouvons obtenir les instructions bytecode de ce générateur en utilisant le dismodule python standard :

>>> 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.decompilingqui 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 decompile()fonctionne la fonction.

La decompile()fonction crée un Decompilerobjet qui implémente le modèle Visiteur. L'instance du décompilateur reçoit 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 stack, 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 nœud AST pour l'expression.

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

Par exemple, voyons comment la sous c.country == 'USA'- expression 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. Appels decompiler.LOAD_FAST('c'). Cette méthode place le Name('c')nœud au sommet de la pile du décompilateur.
  2. Appels decompiler.LOAD_ATTR('country'). Cette méthode prend le Name('c')nœud de la pile, crée le Geattr(Name('c'), 'country')nœud et le place au sommet de la pile.
  3. Appels decompiler.LOAD_CONST('USA'). Cette méthode place le Const('USA')nœud au-dessus de la pile.
  4. Appels decompiler.COMPARE_OP('=='). Cette méthode prend deux nœuds (Getattr et Const) de la pile, puis les place Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) en haut de la pile.

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

Puisque Pony ORM doit décompiler uniquement 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 ne s'agit que d'un tas de boucles imbriquées.

Actuellement, Pony ORM couvre l'ensemble des instructions du générateur, à l'exception de 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 lève l' NotImplementedErrorexception. 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 décompilateur. Au lieu de cela, il obtient l'AST en utilisant la compiler.parsefonction Python standard .

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

Alexandre Kozlovsky
la source
26
Très performant: (1) La décompilation du bytecode est très rapide. (2) Étant donné que chaque requête a un objet code correspondant, cet objet code peut être utilisé comme clé de cache. Pour cette raison, Pony ORM ne traduit chaque requête qu'une seule fois, tandis que Django et SQLAlchemy doivent traduire la même requête encore et encore. (3) Comme Pony ORM utilise le modèle IdentityMap, il met en cache les résultats de la requête dans la même transaction. Il y a un article (en russe) où l'auteur déclare que Pony ORM s'est avéré être 1,5 à 3 fois plus rapide que Django et SQLAlchemy même sans la mise en cache des résultats de la requête: habrahabr.ru/post/188842
Alexander Kozlovsky
3
Est-ce compatible avec le compilateur pypy JIT?
Mzzl
2
Je ne l'ai pas testé, mais un commentateur de Reddit dit qu'il est compatible: tinyurl.com/ponyorm-pypy
Alexander Kozlovsky
9
SQLAlchemy a la mise en cache des requêtes et l'ORM fait un usage intensif de cette fonctionnalité. Il n'est pas activé par défaut car il est vrai que nous n'avons pas de fonctionnalité en place pour lier la construction d'une expression SQL à la position dans le code source qu'elle est déclarée, ce que l'objet code vous donne vraiment. Nous pourrions utiliser l'inspection du châssis de la pile pour obtenir le même résultat, mais c'est juste un peu trop hacky à mon goût. La génération de SQL est dans tous les cas le domaine de performance le moins critique; la récupération des lignes et des changements de comptabilité est.
zzzeek
2
@ randomsurfer_123 probablement pas, nous avons juste besoin d'un peu de temps pour l'implémenter (peut-être une semaine), et il y a d'autres tâches qui sont plus importantes pour nous.
Alexander Kozlovsky