[python] 포니 (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]]
>>>

파이썬에 훌륭한 인트로 스펙 션과 메타 프로그래밍이 내장되어 있다는 것을 알고 있지만,이 라이브러리가 어떻게 전처리없이 생성기 표현식을 번역 할 수 있습니까? 마법처럼 보입니다.

[최신 정보]

Blender는 다음과 같이 썼습니다.

다음은 당신이 추구 하는 파일 입니다. 내부 검사 마법을 사용하여 생성기를 재구성하는 것 같습니다. 파이썬 구문을 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는 Python 생성기를 세 단계로 SQL 쿼리로 변환합니다.

  1. 생성기 바이트 코드 디 컴파일 및 생성기 AST 재 구축 (추상 구문 트리)
  2. Python AST를 “추상 SQL”로 번역-SQL 쿼리의 범용 목록 기반 표현
  3. 추상 SQL 표현을 특정 데이터베이스 종속 SQL 언어로 변환

가장 복잡한 부분은 Pony가 파이썬 표현식의 “의미”를 이해해야하는 두 번째 단계입니다. 첫 번째 단계에 가장 관심이있는 것 같으니 디 컴파일이 어떻게 작동하는지 설명하겠습니다.

이 쿼리를 살펴 보겠습니다.

>>> 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()함수는 파이썬 생성기를 인수로받은 다음 해당 바이트 코드를 분석합니다. 표준 파이썬을 사용하여이 생성기의 바이트 코드 명령을 얻을 수 있습니다.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()함수 Decompiler는 방문자 패턴을 구현 하는 개체를 만듭니다 . 디 컴파일러 인스턴스는 바이트 코드 명령어를 하나씩 가져옵니다. 각 명령어에 대해 디 컴파일러 객체는 자체 메서드를 호출합니다. 이 메서드의 이름은 현재 바이트 코드 명령어의 이름과 같습니다.

파이썬은 표현식을 계산할 때 중간 계산 결과를 저장하는 스택을 사용합니다. 디 컴파일러 객체에도 자체 스택이 있지만이 스택에는 표현식 계산 결과가 아니라 표현식에 대한 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('==')합니다. 이 메서드는 스택에서 두 개의 노드 (Getattr 및 Const)를 가져온 다음 스택 Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))])
    맨 위에 배치합니다.

모든 바이트 코드 명령어가 처리 된 후 디 컴파일러 스택에는 전체 생성기 표현식에 해당하는 단일 AST 노드가 포함됩니다.

Pony ORM은 제너레이터와 람다 만 디 컴파일해야하기 때문에 제너레이터의 명령 흐름이 비교적 간단하기 때문에 복잡하지 않습니다. 이는 중첩 루프의 무리 일뿐입니다.

현재 Pony ORM은 다음 두 가지를 제외하고 전체 생성기 명령어 세트를 다룹니다.

  1. 인라인 if 표현식 : a if b else c
  2. 화합물 비교 : a < b < c

Pony가 이러한 표현을 만나면 NotImplementedError예외가 발생합니다. 그러나이 경우에도 생성기 표현식을 문자열로 전달하여 작동하도록 만들 수 있습니다. 생성기를 문자열로 전달할 때 Pony는 디 컴파일러 모듈을 사용하지 않습니다. 대신 표준 Python compiler.parse함수를 사용하여 AST를 가져옵니다 .

이것이 귀하의 질문에 답하기를 바랍니다.


답변