1. ホーム
  2. python

[解決済み] ポニー(ORM)はどのように機能するのか?

2022-09-26 07:47:48

質問

ポニーORM はジェネレータ式をSQLに変換するという素晴らしいトリックを行います。例を挙げます。

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

Pythonには素晴らしいイントロスペクションやメタプログラミングが組み込まれていますが、このライブラリはどうやって前処理なしでジェネレータ式を翻訳しているのでしょうか?まるで魔法のようです。

[更新しました。]

Blenderが書きました。

以下はそのファイルです。 というファイルです。これは、いくつかのイントロスペクションのウィザードを使ってジェネレータを再構築しているようです。Pythonの構文を100%サポートしているかどうかは分かりませんが、これはかなりクールです。- ブレンダー

ジェネレータ式プロトコルから何か機能を探っているのかと思いきや、このファイルを見てみると ast モジュールが絡んでいる...。いや、まさかその場でプログラムソースを調べているわけではあるまいな。驚きだ...。

@BrenBarn です。の外でジェネレータを呼び出そうとすると select 関数を呼び出すと、その結果は

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

を検査するような、より難解な呪文を唱えているように思えます。 select 関数呼び出しの検査や、Python の抽象構文文法ツリーをオンザフライで処理するような、より難解な呪文を唱えているようです。

私はまだ誰かがそれを説明するのを見たいと思います、ソースは私のウィザードリィレベルをはるかに超えています。

どのように解決するのですか?

ポニーORMの作者はこちらです。

Ponyは3つのステップでPythonのジェネレータをSQLクエリに変換します。

  1. ジェネレータのバイトコードをデコンパイルし、ジェネレータのASTを再構築する。 (抽象構文木)
  2. Python ASTから"abstract SQL"への変換 -- 汎用的な SQLクエリのリストベースの表現
  3. 抽象的なSQL表現を特定の データベース依存のSQL方言に変換

最も複雑な部分は2番目のステップで、PonyはPythonの式の"meaning"を理解しなければなりません。 Pythonの式の意味を理解する必要があります。あなたは最初のステップに最も興味があるようですね。 そこで、デコンパイルがどのように行われるかを説明します。

このクエリを考えてみましょう。

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

これは以下のようなSQLに変換されます。

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

そして以下は、このクエリの結果がプリントアウトされたものです。

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

select() 関数は引数として python ジェネレータを受け取り、そのバイトコードを解析します。 このジェネレータのバイトコード命令は、標準的な 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

ポニーORMは関数 decompile() モジュール内で pony.orm.decompiling これは バイトコードからASTを復元することができます。

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

ここでは、ASTノードのテキスト表現を見ることができます。

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

では、次に decompile() 関数がどのように動作するかを見てみましょう。

decompile() 関数は Decompiler オブジェクトを生成し、Visitorパターンを実装します。 デコンパイラのインスタンスはバイトコード命令を一つずつ取得する。 各命令について、デコンパイラ・オブジェクトはそれ自身のメソッドを呼び出す。 このメソッドの名前は現在のバイトコード命令の名前と同じです。

Pythonが式を計算するとき、計算の中間結果を保存するスタック スタックには計算の中間結果が格納されます。デコンパイラオブジェクトもそれ自身のスタックを持っています。 しかし、このスタックには式の計算結果は格納されません。 式の計算結果ではなく、式のASTノードが格納される。

次のバイトコード命令に対するデコンパイラメソッドが呼ばれると スタックからASTノードを取り出して、それらを結合し を新しいASTノードに結合し、このノードをスタックの一番上に置く。

例えば、部分式 c.country == 'USA' がどのように計算されるか見てみましょう。このとき 対応するバイトコードフラグメントは

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

つまり、デコンパイラオブジェクトは次のようになります。

  1. 呼び出し decompiler.LOAD_FAST('c') . このメソッドは Name('c') ノードをデコンパイラのスタックの一番上に置きます。
  2. 呼び出し decompiler.LOAD_ATTR('country') . このメソッドは Name('c') ノードをスタックから取り出します。 を作成します。 Geattr(Name('c'), 'country') ノードを作成し、それをスタックの一番上に置く。
  3. 呼び出す decompiler.LOAD_CONST('USA') . このメソッドは Const('USA') ノードをスタックの一番上に置きます。
  4. 呼び出し decompiler.COMPARE_OP('==') . このメソッドはスタックから2つのノード(GetattrとConst)を取ります。 そして、スタックから Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) をスタックの一番上に置きます。

すべてのバイトコード命令が処理された後、デコンパイラのスタックには ジェネレータ式全体に対応する単一のASTノードが含まれます。

ポニーORMはジェネレータとラムダのみをデコンパイルする必要があるので、これはそれほど複雑ではありません。 とラムダのみをデコンパイルする必要があるので、これはそれほど複雑ではありません。 ジェネレーターの命令フローは比較的単純で - ジェネレータの命令フローは比較的単純で、ネストされたループの束に過ぎないからです。

現在、Pony ORMは2つのことを除いて、ジェネレータの命令セット全体をカバーしています。

  1. インラインの if 式。 a if b else c
  2. 複合比較。 a < b < c

もしポニーがこのような表現に遭遇した場合、ポニーは NotImplementedError 例外を発生させます。しかし、この場合でも この場合でも、ジェネレータ式を文字列として渡すことで動作させることができます。 ジェネレータを文字列として渡すと、Ponyはデコンパイラモジュールを使用しません。代わりに を使用してASTを取得します。 compiler.parse 関数を用いてASTを取得します。

これがあなたの質問の答えになることを願っています。