どのようにポニー(ORM)はそのトリックを行いますか?


111

Pony 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にはすばらしいイントロスペクションとメタプログラミングが組み込まれていることは知っていますが、このライブラリが前処理なしでジェネレータ式を変換する方法を教えてください。魔法のようです。

[更新]

ブレンダーは書きました:

これがあなたが探しているファイルです。イントロスペクションウィザードを使用してジェネレータを再構築しているようです。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抽象構文文法ツリーの処理など、より難解な呪文を実行しているようです。

誰かに説明してもらいたいのですが、ソースは私の魔法のレベルをはるかに超えています。


おそらくpオブジェクトは、アクセスされているメソッド/プロパティ(例:namestartswith)を調べてSQLに変換するPonyによって実装されたタイプのオブジェクトです。
BrenBarn 2013

3
これがあなたが探しているファイルです。イントロスペクションウィザードを使用してジェネレータを再構築しているようです。Pythonの構文が100%サポートされているかどうかはわかりませんが、これは非常に優れています。
Blender

1
@Blender:私はLISPでこの種のトリックを見てきました-このスタントをPythonで引っ張るのはまったくうんざりです!
Paulo Scardine、2013

回答:


209

Pony ORMの作者はこちらです。

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

  1. ジェネレーターバイトコードの逆コンパイルとジェネレーターASTの再構築(抽象構文ツリー)
  2. Python ASTの「抽象SQL」への変換-SQLクエリの汎用リストベースの表現
  3. 抽象SQL表現を特定のデータベース依存SQL方言に変換する

最も複雑な部分は2番目のステップで、Ponyは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 |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

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

Pony ORMには、バイトコードからASTを復元できるdecompile()モジュール内の関数がありますpony.orm.decompiling

>>> 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()関数は、作成DecompilerVisitorパターンを実装するオブジェクトを、。デコンパイラーのインスタンスは、バイトコード命令を1つずつ取得します。命令ごとに、逆コンパイラオブジェクトは独自のメソッドを呼び出します。このメソッドの名前は、現在のバイトコード命令の名前と同じです。

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ノードが含まれます。

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

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

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

Ponyがそのような式に遭遇すると、NotImplementedError例外が発生します。ただし、この場合でも、ジェネレータ式を文字列として渡すことで機能させることができます。ジェネレータを文字列として渡すと、Ponyは逆コンパイラモジュールを使用しません。代わりに、標準のPythonを使用してASTを取得しますcompiler.parse関数ます。

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


26
非常に高性能:(1)バイトコードの逆コンパイルは非常に高速です。(2)各クエリには対応するコードオブジェクトがあるため、このコードオブジェクトはキャッシュキーとして使用できます。このため、Pony ORMは各クエリを一度だけ翻訳しますが、DjangoとSQLAlchemyは同じクエリを何度も翻訳する必要があります。(3)Pony ORMはIdentityMapパターンを使用するため、同じトランザクション内でクエリ結果をキャッシュします。著者の状態はポニーORMもクエリ結果のキャッシングなし1.5-3倍高速ジャンゴとSQLAlchemyのよりであることが判明していること(ロシア語)のポストがあります:habrahabr.ru/post/188842
アレクサンダーコズロフスキー

3
これはpypy JITコンパイラと互換性がありますか?
Mzzl 2014

2
私はテストしていませんが、一部のRedditコメント投稿者
Alexander Kozlovsky

9
SQLAlchemyにはクエリキャッシュがあり、ORMはこの機能を広範囲に使用します。SQL式の構築を、それが宣言されているソースコードの位置にリンクする機能がないため、デフォルトではオンになりません。これは、コードオブジェクトが実際に提供しているものです。スタックフレームインスペクションを使用して同じ結果を得ることができますが、それは私の好みには少しハック過ぎです。SQLの生成は、いかなる場合でも最も重要なパフォーマンス領域です。行のフェッチとブックキーピングの変更です。
zzzeek 2014

2
@ randomsurfer_123はおそらくそうではありません。それを実装するための時間(おそらく1週間)が必要なだけであり、他にも重要なタスクがあります。
アレクサンダーコズロフスキー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.