Como Pony (ORM) faz seus truques?

111

Pony ORM faz o belo truque de converter uma expressão geradora em SQL. Exemplo:

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

Eu sei que Python tem introspecção e metaprogramação maravilhosas embutidas, mas como esta biblioteca é capaz de traduzir a expressão geradora sem pré-processamento? Parece mágica.

[atualizar]

O Blender escreveu:

Aqui está o arquivo que você procura. Parece reconstruir o gerador usando alguma magia de introspecção. Não tenho certeza se ele suporta 100% da sintaxe do Python, mas isso é muito legal. - Liquidificador

Eu estava pensando que eles estavam explorando algum recurso do protocolo de expressão do gerador, mas olhando este arquivo e vendo o astmódulo envolvido ... Não, eles não estão inspecionando o código-fonte do programa em tempo real, estão? Surpreendente...

@BrenBarn: Se eu tentar chamar o gerador fora do select chamada de função, o resultado é:

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

Parece que eles estão fazendo encantamentos mais misteriosos, como inspecionar o select chamada de função e processar a árvore gramatical de sintaxe abstrata do Python em tempo real.

Eu ainda gostaria de ver alguém explicando isso, a fonte está muito além do meu nível de magia.

Paulo Scardine
fonte
Presumivelmente, o pobjeto é um objeto de um tipo implementado por Pony que olha para o que métodos / propriedades estão sendo acessados nele (por exemplo, name, startswith) e os converte em SQL.
BrenBarn de
3
Aqui está o arquivo que você procura. Parece reconstruir o gerador usando alguma magia de introspecção. Não tenho certeza se ele suporta 100% da sintaxe do Python, mas isso é muito legal.
Blender
1
@Blender: Eu vi esse tipo de truque no LISP - fazer essa proeza em Python é simplesmente doentio!
Paulo Scardine de

Respostas:

209

O autor Pony ORM está aqui.

O Pony traduz o gerador Python em consulta SQL em três etapas:

  1. Descompilação do bytecode do gerador e reconstrução do gerador AST (árvore de sintaxe abstrata)
  2. Tradução de Python AST em "SQL abstrato" - representação universal baseada em lista de uma consulta SQL
  3. Converter representação SQL abstrata em dialeto SQL dependente de banco de dados específico

A parte mais complexa é a segunda etapa, onde Pony deve entender o "significado" das expressões Python. Parece que você está mais interessado na primeira etapa, então deixe-me explicar como funciona a descompilação.

Vamos considerar esta consulta:

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

Que será traduzido no seguinte SQL:

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

E a seguir está o resultado dessa consulta que será impressa:

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

A select()função aceita um gerador Python como argumento e analisa seu bytecode. Podemos obter instruções de bytecode deste gerador usando o dismódulo Python padrão :

>>> 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 tem a função decompile()dentro do módulo pony.orm.decompilingque pode restaurar um AST do bytecode:

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

Aqui, podemos ver a representação textual dos nós AST:

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

Vamos agora ver como o decompile() função funciona.

A decompile()função cria umDecompiler objeto, que implementa o padrão Visitor. A instância do decompiler obtém instruções de bytecode uma a uma. Para cada instrução, o objeto descompilador chama seu próprio método. O nome deste método é igual ao nome da instrução bytecode atual.

Quando o Python calcula uma expressão, ele usa pilha, que armazena um resultado intermediário do cálculo. O objeto descompilador também tem sua própria pilha, mas essa pilha não armazena o resultado do cálculo da expressão, mas o nó AST da expressão.

Quando o método descompilador para a próxima instrução de bytecode é chamado, ele pega os nós AST da pilha, combina-os em um novo nó AST e então coloca esse nó no topo da pilha.

Por exemplo, vamos ver como a subexpressão c.country == 'USA'é calculada. O fragmento de bytecode correspondente é:

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

Portanto, o objeto descompilador faz o seguinte:

  1. Chamadas decompiler.LOAD_FAST('c'). Este método coloca o Name('c')nó no topo da pilha do descompilador.
  2. Chamadas decompiler.LOAD_ATTR('country'). Este método tira o Name('c')nó da pilha, cria o Geattr(Name('c'), 'country')nó e o coloca no topo da pilha.
  3. Chamadas decompiler.LOAD_CONST('USA'). Este método coloca oConst('USA') nó no topo da pilha.
  4. Chamadas decompiler.COMPARE_OP('=='). Este método pega dois nós (Getattr e Const) da pilha e, em seguida, coloca Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) no topo da pilha.

Depois que todas as instruções de bytecode são processadas, a pilha do descompilador contém um único nó AST que corresponde a toda a expressão do gerador.

Como o Pony ORM precisa descompilar apenas geradores e lambdas, isso não é tão complexo, porque o fluxo de instruções para um gerador é relativamente simples - é apenas um monte de loops aninhados.

Atualmente Pony ORM cobre todo o conjunto de instruções do gerador, exceto duas coisas:

  1. Expressões if inline: a if b else c
  2. Comparações de compostos: a < b < c

Se Pony encontrar tal expressão, ele gerará a NotImplementedErrorexceção. Mas, mesmo neste caso, você pode fazer funcionar passando a expressão do gerador como uma string. Quando você passa um gerador como string, o Pony não usa o módulo descompilador. Em vez disso, ele obtém o AST usando o Python padrãocompiler.parse função .

espero que isso responda sua pergunta.

Alexander Kozlovsky
fonte
26
Muito desempenho: (1) A descompilação do bytecode é muito rápida. (2) Como cada consulta tem um objeto de código correspondente, este objeto de código pode ser usado como uma chave de cache. Por causa disso, Pony ORM traduz cada consulta apenas uma vez, enquanto Django e SQLAlchemy têm que traduzir a mesma consulta repetidamente. (3) Como o Pony ORM usa o padrão IdentityMap, ele armazena em cache os resultados da consulta na mesma transação. Há um post (em russo) onde o autor afirma que o Pony ORM acabou sendo 1,5-3 vezes mais rápido que o Django e o SQLAlchemy, mesmo sem o cache do resultado da consulta: habrahabr.ru/post/188842
Alexander Kozlovsky
3
Isso é compatível com o compilador JIT pypy?
Mzzl
2
Não testei, mas alguns comentaristas do Reddit dizem que é compatível: tinyurl.com/ponyorm-pypy
Alexander Kozlovsky
9
SQLAlchemy possui cache de consulta e o ORM faz uso extensivo deste recurso. Não está ativado por padrão porque é verdade que não temos um recurso para vincular a construção de uma expressão SQL à posição no código-fonte que está declarada, que é o que o objeto de código está realmente fornecendo. Poderíamos usar a inspeção de estrutura de pilha para obter o mesmo resultado, mas isso é um pouco estranho para o meu gosto. A geração de SQL é a área de desempenho menos crítica em qualquer caso; buscar linhas e mudanças na contabilidade é.
zzzeek
2
@ randomsurfer_123 provavelmente não, só precisamos de algum tempo para implementá-lo (talvez uma semana), e há outras tarefas que são mais importantes para nós.
Alexander Kozlovsky