web-dev-qa-db-ja.com

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

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には素晴らしいイントロスペクションとメタプログラミングが組み込まれていますが、このライブラリは前処理なしでジェネレータ式をどのように変換できるか?魔法のように見えます。

[更新]

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抽象構文文法ツリーのオンザフライでの処理)など、より難解な呪文を行っているようです。

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

106
Paulo Scardine

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

PonyはPythonジェネレーターを3つのステップで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 |[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
_

Pony ORMには、モジュール_pony.orm.decompiling_内に関数decompile()があり、バイトコードから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()関数は、Visitorパターンを実装するDecompilerオブジェクトを作成します。デコンパイラインスタンスは、バイトコード命令を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_

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

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

203