O pré-processador do Python
(pydong.org)- O Python pode usar as duas primeiras linhas de comentários mágicos de codificação da fonte e um codec personalizado para alterar o conteúdo de um arquivo antes da execução ou até substituí-lo por um código completamente diferente
- Um codec personalizado pode ser registrado durante a inicialização do interpretador por meio da execução de
importem um arquivo.pthde configuração de caminho, adicionando uma função de busca comcodecs.register - A implementação do codec exige
decode(data: bytes) -> tuple[str, int]e um decodificador incremental; se as exceções não forem tratadas, pode aparecer apenasSyntaxError: encoding problem: your_codecem vez da causa real - Operadores de incremento/decremento
++/--, Python baseado em chaves, execução de C/C++ viacppyye até validação de TOML com JSON Schema podem ser implementados pelo mesmo ponto de entrada - Além de exemplos brincalhões, isso também pode ser usado em extensões e backports de Python como
pythonql,future-typing,future-fstringsefuture-annotations, enquantomagic_codecreduz trabalho repetitivo
Usando a codificação da fonte como ponto de entrada do pré-processamento
- De acordo com a PEP-0263, é possível definir a codificação do código-fonte em uma das duas primeiras linhas de um arquivo Python
- Exemplos:
# coding=utf8,# -*- coding: utf8 -*-,# vim: set fileencoding=utf8 :
- Exemplos:
- A linha mágica precisa corresponder à regex
^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)- O nome do codec precisa corresponder a
[-_.a-zA-Z0-9]+
- O nome do codec precisa corresponder a
- Um codec personalizado não precisa apenas decodificar o código-fonte: ele também pode modificar a string de origem antes de entregá-la ao interpretador Python
Registrando o codec com arquivo .pth
- Quando o interpretador Python inicia sem a opção
-S, o pacotesiteé carregado durante a inicialização - Os arquivos
.pthde configuração de caminho dentro desite-packagesadicionam seu conteúdo ao caminho de busca de módulos, exceto linhas em branco e linhas que começam com# - Segundo a documentação do Python, linhas em que
importé seguido por espaço ou tabulação são executadas- Exemplo: colocar
import packagename.register_codecempackagename.pthfaz esse módulo ser importado durante a inicialização
- Exemplo: colocar
- O módulo importado pode chamar
codecs.registerpara registrar uma função de busca de codec- Como o
importé executado só uma vez, a função de busca também é registrada só uma vez
- Como o
Implementando um codec personalizado
- Um codec personalizado precisa de duas coisas
decode(data: bytes) -> tuple[str, int]- uma classe de decodificador incremental
- A função
decodepode usarcodecs.utf_8_decodepara fazer a decodificação UTF-8 real e então passar a string resultante para a função de pré-processamento - Se exceções internas do codec não forem capturadas, pode ser exibido apenas
SyntaxError: encoding problem: your_codecem vez de um traceback normal- É melhor imprimir diretamente a exceção gerada na função de pré-processamento com
traceback.print_exc()e depois relançá-la
- É melhor imprimir diretamente a exceção gerada na função de pré-processamento com
- O decodificador incremental pode acumular o arquivo inteiro em buffer e executar o pré-processamento só na chamada final de
decode- A implementação de exemplo herda de
codecs.BufferedIncrementalDecodere processa apenas quandofinalé verdadeiro emdecode(self, data, final=False)
- A implementação de exemplo herda de
- O resultado do pré-processamento não precisa usar obrigatoriamente o conteúdo original do arquivo; ele pode retornar código Python totalmente arbitrário
- Porém, a primeira linha é esperada como linha mágica e por isso é removida, e o resultado precisa ser Python válido
Exemplos de extensão da sintaxe do Python
-
Operadores de incremento e decremento
++e--- O Python não tem operadores unários de incremento/decremento
x++ex--não são sintaticamente válidos++xe--xsão sintaticamente válidos, mas se tornam chamadas ax.__pos__().__pos__()ex.__neg__().__neg__()respectivamente- O pré-processador pode modificar o fluxo de tokens para fazê-los funcionar como operadores de incremento/decremento
x++→(x, x := x + 1)[0]x--→(x, x := x - 1)[0]++x→(x, x := x + 1)[1]--x→(x, x := x - 1)[1]
- Essa transformação usa o operador walrus do Python, isto é, a expressão de atribuição
- Uma simples substituição de tokens pode falhar em expressões como
x++ - -y; usar parênteses como emx++ - (-y)pode reduzir a ambiguidade - incdec.py faz a substituição com regex, mas pode continuar frágil mesmo tentando evitar substituições dentro de literais de string
- Há uma implementação que modifica diretamente o fluxo de tokens em magic.incdec
-
Python baseado em chaves
from __future__ import bracesgeraSyntaxError: not a chance- O pré-processador pode modificar o fluxo de tokens para converter escopo baseado em chaves em Python baseado em indentação
- O fluxo de implementação é o seguinte
- gerar tokens com
tokenize.generate_tokens - fornecer a string de entrada linha por linha com
readlinedeio.StringIO - remover os tokens
INDENTeDEDENTexistentes - ao encontrar
{, aumentar o nível de indentação e emitir: - ao encontrar
}, diminuir o nível de indentação - depois de
NL, adicionar um tokenINDENTcorrespondente ao nível atual de indentação
- gerar tokens com
- Para reduzir conflito com literais de dicionário do Python, é possível ajustar o nível de indentação só quando houver quebra de linha depois de
{, e tratar como fechamento de escopo só quando houver quebra de linha antes de} - Dicionários multilinha podem funcionar usando barra invertida, pois assim não surgem tokens de newline dentro das chaves
Executando outras linguagens com Python
-
C e C++
- Linguagens que usam
#para comentários, como scripts shell, scripts CMake, PHP e Ruby, permitem inserir facilmente uma linha mágica de codificação junto com shebang - Em C e C++, os comentários são
/* ... */ou// ..., mas diretivas de pré-processador começam com#, então podem corresponder à regex de codificação - A linha mágica de exemplo é válida também em código C/C++ e ao mesmo tempo corresponde ao padrão de codificação do Python
#define CODEC "coding:magic.cpp"
- Com cppyy, o Python pode interpretar código C/C++ e gerar bindings
- Internamente,
cppyyusacling
- Internamente,
- O resultado do pré-processamento fica aproximadamente como o código Python abaixo
import cppyycppyy.cppdef("<input source file content>")from cppyy.gbl import main- chamar
main()de C/C++ quando__name__ == "__main__"
- A implementação de exemplo está em magic.cpp
- Linguagens que usam
Usando como ferramenta de validação de TOML
- Como TOML inicia comentários com
#, é possível inserir uma linha mágica de codificação como# coding: magic.toml - Se o resultado do pré-processamento for convertido em um script Python de validação, o interpretador Python pode ser usado como uma ferramenta de validação de TOML
- O exemplo de validação usa os seguintes módulos
tomllibpara ler o arquivo TOMLjsonpara ler o arquivo JSON Schema- jsonschema para validar
- Exemplo de execução:
python tests/toml/data_valid.toml -s tests/toml/schema.json- Se for válido, imprime
Successfully validated.
- Em um exemplo de TOML inválido, aparece um erro de validação informando que a string
'20'dentro do arrayscoresnão é um número - A implementação de exemplo está em magic.toml
Uso real e magic_codec
- Combinar codecs personalizados com arquivos
.pthpermite alterar bastante o comportamento do interpretador Python - A maioria dos exemplos é mais pela diversão, mas também existem casos de uso reais
- pythonql: extensão de linguagem de consultas para Python
- future-typing: backport de type hints genéricos e da sintaxe de união
|para Python 3.6+ - future-fstrings
- future-annotations
- Se você não quiser mexer diretamente em
site-packagesnem escrever manualmente arquivos.pthe código repetitivo, pode usar magic_codec - As extensões de
magic_codecpodem ser criadas como pacotes Python com prefixomagic_- Se o codec do arquivo for definido como
magic_foo, o pacotemagic_fooserá carregado - Depois, é verificado se esse pacote tem uma função
preprocess
- Se o codec do arquivo for definido como
- A assinatura esperada de
preprocessé a seguintedef preprocess(data: str) -> str:
- Há exemplos de extensão em example/
1 comentários
Comentários do Hacker News
A mensagem de erro engraçada
SyntaxError: not a chance, exibida ao executarfrom __future__ import braces, está hardcoded no CPython desde 2001https://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
O autor, Jeremy Hylton, hoje trabalha no Google como Principal Engineer responsável pela qualidade da busca com IA, e é bem impressionante ver como, em 24 anos, sua carreira foi de comemorar em tom de brincadeira uma sintaxe proibida até sistemas universais de consulta que não exigem sintaxe dedicada
break rust;causava um erro interno do compilador no compilador Rust. Fico curioso para saber quantos easter eggs parecidos existem em outras linguagensAcho um engano imaginar que esse tipo de hacking brincalhão e informal de hobby seja um mundo separado do desenvolvimento profissional real
https://news.ycombinator.com/item?id=41314393
Eu achava que brincar com import hook era a forma mais criativa de ser demitido, mas isso era ingenuidade minha. É uma pena que, por causa da regex do codec, não dê para trollar direito com algo como
μtf8; agora só resta usar import hook, pré-processador esys.settracepara monkeypatchar todas as funções para a função chamada imediatamente antes e trocar stdout com stderr a cada 17 minutosHá bons motivos para o Python não expor de propósito os hooks de pré-processador, e acho que isso é algo de que um adulto razoável deveria manter distância
Mas, por outro lado, eu não quero me envolver com adultos razoáveis. Parece que daria para fazer muita coisa realmente divertida
public/private, e aparentemente também deixa aberta toda sorte de magia de metaprogramação. Se realmente estivesse preocupado com “adultos razoáveis”, algumas dessas decisões de design pareceriam meio estranhas :)Isso parece conveniente e realmente útil. Quando eu fazia hacks absurdos de import, normalmente importava o módulo, reescrevia o código com o módulo
ast, davaexece então enfiava umexit(), mas com um pré-processador isso seria bem mais agradável de usarAntes de todos os
dictgarantirem ordem, eu usava mais para reescrever AST e trocar literais de lista por chamadas de ordered dict, e isso de fato era útilGosto da flexibilidade do Python. A coisa mais amaldiçoada que já fiz foi modificar strings in-place, e acabei até abusando de
mmappara escrever um script que modificava a si próprio. Agora parece que preciso escrever um interpretador Lisp como producerctypes?O melhor caso de uso que encontrei até agora é o pyxl, inspirado em JSX: https://github.com/dropbox/pyxl
Dá para escrever código assim
# coding: pyxlprint Hello World!Fico pensando se isso poderia ter sido usado para lidar melhor com a transição do Python 2 para o 3. Por exemplo,
# coding: six.python2poderia ajustar código Python 2 para virar código Python 3 válido, ou# coding: six.python3poderia transformar código Python 3 para rodar no Python 2Também talvez desse para adicionar ou remover prefixos como
b"..."ouu"..."dict, apontavam para a mesma entrada; já no Python 3,bytesestrcom o mesmo conteúdo ASCII apontam para entradas diferentes dentro do mesmodictHouve mudanças ainda mais complicadas. Vários recursos embutidos, como
.keys()e.values(), retornam listas no Python 2, mas iteradores no Python 3. Se você tenta traduzir o código de forma segura com utilitários dosixou outros contornos, o código fica muito verboso. Na maior parte das vezes esses valores são usados uma vez só, mas às vezes são usados duas vezesSe houvesse uma ferramenta para reescrever o código no momento do import, eu ainda acharia melhor simplesmente dar commit no código convertido e ir limpando aos poucos. A parte difícil são mudanças de comportamento como
strversusbytes, que afetam até trechos de código distantesDependências criadas por essa estratégia de coding hook são detectadas por
pip freezeouuv?Se não, parece que vai ser divertido :). Se alguém colocou algo assim, é praticamente garantido que existam outras armadilhas, então provavelmente seria mais fácil reescrever a biblioteca do que lutar contra esse tipo de dragão
Seria bem engraçado criar um Python em pseudocódigo e deixar uma LLM decodificar isso. Obviamente seria horrível, mas talvez divertido