Ir para o conteúdo
AZ Tools

Inspetor de bytecode .pyc do Python

Um .pyc é a forma compilada que o CPython escreve ao lado do fonte, dentro de __pycache__, e tem exatamente duas partes. Os primeiros dezesseis bytes são o cabeçalho: um número mágico de quatro bytes que nomeia a versão exata do CPython que escreveu o arquivo (não a que você tem instalada, e sim a que o compilou), seguido de uma palavra de flags. O bit 0 decide como o cache é invalidado. Em zero, os oito bytes seguintes guardam a data de modificação do fonte e o tamanho dele, e o sistema de importação descarta o cache assim que qualquer um dos dois deixa de bater com o .py em disco. Em um, como permite a PEP 552, esses oito bytes guardam um hash do fonte, e o bit 1 diz se o interpretador o reconfere a cada importação ou se confia cegamente, que é o que uma compilação reproduzível quer. Atrás do cabeçalho há um único objeto de código serializado pelo marshal, e cada função, lambda, compreensão e corpo de classe do módulo está aninhado dentro das constantes dele como outro objeto de código: por isso a árvore aqui desce vários níveis. De cada um você vê os nomes que ele alcança, as variáveis locais, de célula e livres, a contagem de argumentos, o tamanho da pilha, os bits de flag decodificados, cada constante escrita como repr() a escreveria e um desmontador com EXTENDED_ARG dobrado na instrução a que pertence e todo salto resolvido para um deslocamento absoluto. A numeração dos opcodes depende da versão e muda quase a cada lançamento, então a tabela é escolhida pelo número mágico e nomeada na tela: da 3.7 à 3.13, e um arquivo de qualquer outra versão tem o cabeçalho lido e o corpo deixado em paz, em vez de decodificado com a tabela errada. O que ele não faz é devolver o seu fonte. Isto não é um descompilador: nomes, docstrings e constantes sobrevivem inteiros, mas o fluxo que você vê é bytecode, não o laço que você escreveu, e comentários e formatação foram descartados pelo compilador muito antes de este arquivo existir. A tabela de números de linha e, a partir da 3.11, a de exceções são puladas sem decodificação.

Como usar

  1. Solte um .pyc de uma pasta __pycache__: o nome costuma parecer modulo.cpython-311.pyc.
  2. Olhe primeiro "Escrito por": é a versão do CPython que compilou o arquivo, e um interpretador com outro número mágico ignora o cache e recompila a partir do fonte.
  3. Confira o modo de invalidação. Um arquivo por data carrega a data e o tamanho do fonte; um por hash carrega um hash do fonte, e a variante sem verificação nunca mais é comparada com ele.
  4. Escolha um objeto de código na árvore. No topo está o módulo, e cada função, corpo de classe, lambda e (antes da 3.12) compreensão fica pendurado sob aquilo que a define.
  5. Leia o desmontador do objeto selecionado: a coluna Resolvido troca índices por nomes, locais e constantes, e >> marca uma instrução onde algum salto cai.

Perguntas frequentes

Editei o fonte, mas o Python continua rodando o código antigo. Por quê?
Um .pyc invalidado por data guarda a modificação do fonte com precisão de um segundo e o tamanho em bytes, e o CPython reaproveita o cache enquanto os dois continuarem batendo. Duas situações derrotam isso. Se uma edição cair dentro do mesmo segundo da data registrada e deixar o arquivo com o mesmo comprimento — uma troca de um caractere num arquivo gerado, por exemplo — nada parece diferente e o cache velho é usado. E restaurar arquivos com uma ferramenta que preserva datas, ou trocar para um branch que as recua, pode deixar um .pyc mais novo que um fonte com o qual ele já não corresponde. Apagar a pasta __pycache__ resolve os dois casos; rodar com -B ou com PYTHONDONTWRITEBYTECODE evita que os arquivos sejam escritos.
Qual a diferença entre um .pyc por hash verificado e um sem verificação?
A PEP 552 trocou a data por um hash de oito bytes do fonte, para que a compilação deixe de depender das datas dos arquivos: o mesmo fonte produz um .pyc idêntico byte a byte em qualquer máquina, que é o que compilações reproduzíveis exigem. O bit 1 da palavra de flags escolhe o comportamento: verificado quer dizer que o interpretador calcula o hash do fonte a cada importação e recompila se diferir; sem verificação quer dizer que ele não olha o fonte de jeito nenhum. O segundo caso serve quando algo fora do Python garante que está atualizado, como uma imagem de contêiner construída de uma vez, e é uma armadilha em qualquer outro lugar, porque editar o fonte não surte efeito algum até o arquivo ser regerado à mão.
Dá para recuperar o fonte original a partir de um .pyc?
Com esta ferramenta não, e com nenhuma por completo. O compilador guarda tudo o que precisa para executar: cada nome, cada constante, as docstrings, os nomes dos argumentos e a primeira linha de cada função, de modo que um .pyc vaza muito mais do que se imagina e nunca deve ser tratado como ofuscação. O que ele não guarda é o texto: comentários, linhas em branco, formatação e o formato exato das expressões somem. Descompiladores como uncompyle6 ou decompyle3 reconstroem um fonte plausível a partir do fluxo de instruções, mas ficam anos atrás da linguagem e param por volta da 3.9, porque os compiladores novos reorganizam o controle de fluxo de um jeito que não volta mais um para um.
Por que o mesmo fonte gera bytecode completamente diferente em duas versões do Python?
Porque o conjunto de instruções é um detalhe de implementação que o CPython reescreve à vontade. A 3.11 introduziu caches em linha — bytes reais no fluxo, pertencentes à instrução anterior — e moveu o tratamento do frame para opcodes novos como RESUME e MAKE_CELL. A 3.12 embutiu as compreensões de lista e de conjunto, que deixaram de ser objetos de código separados. A 3.13 renumerou quase tudo de novo. É exatamente para isso que existe o número mágico: um .pyc só vale para o lançamento que o escreveu, e qualquer outro interpretador o trata como inexistente.
Por que os deslocamentos pulam mais de dois, e o que é EXTENDED_ARG?
Cada instrução ocupa dois bytes, um de opcode e um de argumento, então um argumento maior que 255 é montado por uma ou mais instruções EXTENDED_ARG à frente dele, cada uma contribuindo com mais oito bits. Aqui elas são dobradas na instrução seguinte, exatamente como o dis faz, e por isso você verá argumentos na casa dos milhares numa única linha. A partir da 3.11 o fluxo também contém espaços de cache em linha que pertencem à instrução anterior e guardam o estado de especialização em tempo de execução; eles são pulados em vez de listados, então os deslocamentos avançam mais de dois. Num .pyc em disco esses espaços são sempre zero: a especialização acontece na memória, e os opcodes acelerados nunca são escritos em arquivo.
O que são variáveis de célula e livres, e por que um nome aparece em duas listas?
Um fechamento precisa que a variável viva mais que o frame que a criou, então o compilador a coloca numa célula: a função que a define a lista entre suas variáveis de célula, e cada função aninhada que a lê lista o mesmo nome entre suas variáveis livres. A partir da 3.11 as duas ficam num único array com um byte de tipo por entrada, e é por isso que um argumento capturado por uma função aninhada aparece ao mesmo tempo como local e como variável de célula: ele é mesmo as duas coisas, e a função começa com uma instrução MAKE_CELL que move o argumento para a célula dele.

Ferramentas relacionadas