Jak Pony (ORM) wykonuje swoje sztuczki?

111

Pony ORM robi niezłą sztuczkę konwertowania wyrażenia generatora na SQL. Przykład:

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

Wiem, że Python ma wbudowane wspaniałe funkcje introspekcji i metaprogramowania, ale w jaki sposób ta biblioteka jest w stanie przetłumaczyć wyrażenie generatora bez wstępnego przetwarzania? Wygląda jak magia.

[aktualizacja]

Blender napisał:

Oto plik , którego szukasz. Wydaje się, że rekonstruuje generator przy użyciu pewnej magii introspekcji. Nie jestem pewien, czy obsługuje 100% składni Pythona, ale jest całkiem fajny. - Blender

Myślałem, że badają jakąś funkcję z protokołu wyrażeń generatora, ale patrząc na ten plik i widząc astzaangażowany moduł ... Nie, oni nie sprawdzają źródła programu w locie, prawda? Niesamowite ...

@BrenBarn: Jeśli spróbuję wywołać generator poza selectwywołaniem funkcji, wynik jest:

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

Wygląda na to, że wykonują bardziej tajemne zaklęcia, takie jak sprawdzanie selectwywołania funkcji i przetwarzanie drzewa gramatyki składni abstrakcyjnej w Pythonie w locie.

Nadal chciałbym zobaczyć, jak ktoś to wyjaśnia, źródło jest daleko poza moim poziomem magii.

Paulo Scardine
źródło
Przypuszczalnie pobiekt jest obiektem typu zaimplementowanego przez Pony, który sprawdza, jakie metody / właściwości są na nim dostępne (np. name, startswith) I konwertuje je na SQL.
BrenBarn
3
Oto plik, którego szukasz. Wydaje się, że rekonstruuje generator przy użyciu pewnej magii introspekcji. Nie jestem pewien, czy obsługuje 100% składni Pythona, ale jest całkiem fajny.
Blender
1
@Blender: Widziałem tego rodzaju sztuczkę w LISP-ie - wykonanie tego wyczynu w Pythonie jest po prostu chore!
Paulo Scardine

Odpowiedzi:

209

Autor Pony ORM jest tutaj.

Pony tłumaczy generator Pythona na zapytanie SQL w trzech krokach:

  1. Dekompilacja kodu bajtowego generatora i przebudowa generatora AST (abstrakcyjne drzewo składniowe)
  2. Tłumaczenie języka Python AST na „abstrakcyjny SQL” - uniwersalna reprezentacja zapytania SQL oparta na liście
  3. Konwersja abstrakcyjnej reprezentacji SQL na określony dialekt SQL zależny od bazy danych

Najbardziej złożoną częścią jest drugi krok, w którym Pony musi zrozumieć „znaczenie” wyrażeń Pythona. Wygląda na to, że najbardziej interesuje Cię pierwszy krok, więc pozwól mi wyjaśnić, jak działa dekompilacja.

Rozważmy to zapytanie:

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

Który zostanie przetłumaczony na następujący SQL:

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

A poniżej wynik tego zapytania, który zostanie wydrukowany:

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()Funkcja przyjmuje generator pytona jako argumentu, i analizuje jego bajtowego. Możemy uzyskać instrukcje kodu bajtowego tego generatora za pomocą standardowego dismodułu Pythona :

>>> 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 ma funkcję decompile()w module, pony.orm.decompilingktóra może przywrócić AST z kodu bajtowego:

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

Tutaj możemy zobaczyć tekstową reprezentację węzłów AST:

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

Zobaczmy teraz, jak decompile()działa ta funkcja.

decompile()Funkcja tworzy Decompilerobiekt, który implementuje odwiedzający. Instancja dekompilatora pobiera instrukcje kodu bajtowego jedna po drugiej. Dla każdej instrukcji obiekt dekompilatora wywołuje własną metodę. Nazwa tej metody jest taka sama, jak nazwa bieżącej instrukcji kodu bajtowego.

Kiedy Python oblicza wyrażenie, używa stosu, który przechowuje pośredni wynik obliczeń. Obiekt dekompilatora również ma swój własny stos, ale ten stos przechowuje nie wynik obliczenia wyrażenia, ale węzeł AST dla wyrażenia.

Kiedy wywoływana jest metoda dekompilacji dla następnej instrukcji kodu bajtowego, pobiera węzły AST ze stosu, łączy je w nowy węzeł AST, a następnie umieszcza ten węzeł na szczycie stosu.

Na przykład zobaczmy, jak c.country == 'USA'obliczane jest podwyrażenie . Odpowiedni fragment kodu bajtowego to:

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

Tak więc obiekt dekompilatora wykonuje następujące czynności:

  1. Połączenia decompiler.LOAD_FAST('c'). Ta metoda umieszcza Name('c')węzeł na szczycie stosu dekompilatora.
  2. Połączenia decompiler.LOAD_ATTR('country'). Ta metoda pobiera Name('c')węzeł ze stosu, tworzy Geattr(Name('c'), 'country')węzeł i umieszcza go na szczycie stosu.
  3. Połączenia decompiler.LOAD_CONST('USA'). Ta metoda umieszczaConst('USA') węzeł na szczycie stosu.
  4. Połączenia decompiler.COMPARE_OP('=='). Ta metoda pobiera dwa węzły (Getattr i Const) ze stosu, a następnie umieszcza je Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) na szczycie stosu.

Po przetworzeniu wszystkich instrukcji kodu bajtowego stos dekompilatora zawiera pojedynczy węzeł AST, który odpowiada całemu wyrażeniu generatora.

Ponieważ Pony ORM musi dekompilować tylko generatory i lambdy, nie jest to takie skomplikowane, ponieważ przepływ instrukcji dla generatora jest stosunkowo prosty - jest to po prostu kilka zagnieżdżonych pętli.

Obecnie Pony ORM obejmuje cały zestaw instrukcji generatora z wyjątkiem dwóch rzeczy:

  1. Wbudowane wyrażenia if: a if b else c
  2. Porównania złożone: a < b < c

Jeśli Pony napotka takie wyrażenie, zgłosi NotImplementedErrorwyjątek. Ale nawet w tym przypadku możesz sprawić, by działało, przekazując wyrażenie generatora jako ciąg. Kiedy przekazujesz generator jako ciąg znaków, Pony nie używa modułu dekompilatora. Zamiast tego pobiera AST za pomocą standardowej compiler.parsefunkcji Pythona .

Mam nadzieję, że to odpowiada na twoje pytanie.

Aleksandra Kozłowskiego
źródło
26
Bardzo wydajne: (1) Dekompilacja kodu bajtowego jest bardzo szybka. (2) Ponieważ każde zapytanie ma odpowiadający mu obiekt kodu, ten obiekt kodu może być używany jako klucz pamięci podręcznej. Z tego powodu Pony ORM tłumaczy każde zapytanie tylko raz, podczas gdy Django i SQLAlchemy muszą wielokrotnie tłumaczyć to samo zapytanie. (3) Ponieważ Pony ORM używa wzorca IdentityMap, buforuje wyniki zapytania w ramach tej samej transakcji. Jest post (w języku rosyjskim), w którym autor stwierdza, że ​​Pony ORM okazał się 1,5-3 razy szybszy niż Django i SQLAlchemy, nawet bez buforowania wyników zapytania: habrahabr.ru/post/188842
Alexander Kozlovsky,
3
Czy jest to zgodne z kompilatorem Pypy JIT?
Mzzl
2
Nie testowałem tego, ale jakiś komentator Reddit mówi, że jest kompatybilny: tinyurl.com/ponyorm-pypy
Alexander Kozlovsky
9
SQLAlchemy ma buforowanie zapytań, a ORM szeroko wykorzystuje tę funkcję. Nie jest on domyślnie włączony, ponieważ jest prawdą, że nie mamy funkcji, która łączyłaby konstrukcję wyrażenia SQL z pozycją w kodzie źródłowym, którą jest zadeklarowana, co tak naprawdę daje obiekt kodu. Moglibyśmy użyć inspekcji ramki stosu, aby uzyskać ten sam wynik, ale jest to trochę zbyt hakerskie jak na mój gust. Generowanie kodu SQL jest w każdym przypadku najmniej krytycznym obszarem wydajności; pobieranie wierszy i zmiany w księgowości jest.
zzzeek
2
@ randomsurfer_123 raczej nie, potrzebujemy trochę czasu na jego realizację (może tydzień), a są inne zadania, które są dla nas ważniejsze.
Alexander Kozlovsky