É possível "hackear" a função de impressão do Python?

151

Nota: Esta pergunta é apenas para fins informativos. Estou interessado em ver até que ponto é possível entrar no interior do Python.

Não faz muito tempo, iniciou-se uma discussão sobre uma certa questão sobre se as seqüências passadas para imprimir instruções poderiam ser modificadas após / durante a chamada print. Por exemplo, considere a função:

def print_something():
    print('This cat was scared.')

Agora, quando printexecutado, a saída para o terminal deve exibir:

This dog was scared.

Observe que a palavra "gato" foi substituída pela palavra "cachorro". De alguma forma, algo em algum lugar foi capaz de modificar esses buffers internos para alterar o que foi impresso. Suponha que isso seja feito sem a permissão explícita do autor do código original (portanto, hackers / seqüestros).

Este comentário do sábio @abarnert, em particular, me fez pensar:

Existem algumas maneiras de fazer isso, mas todas são muito feias e nunca devem ser feitas. A maneira menos feia é provavelmente substituir o codeobjeto dentro da função por outro com uma co_consts lista diferente . O próximo é provavelmente acessar a API C para acessar o buffer interno do str. [...]

Então, parece que isso é realmente possível.

Aqui está minha maneira ingênua de abordar esse problema:

>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.

Obviamente, execé ruim, mas isso realmente não responde à pergunta, porque na verdade não modifica nada durante quando / depois print é chamado.

Como isso seria feito como o @abarnert explicou?

cs95
fonte
3
A propósito, o armazenamento interno para ints é muito mais simples que strings e flutua ainda mais. E, como bônus, é muito mais óbvio por que é uma má ideia alterar o valor de 42para do 23que por que é uma má idéia alterar o valor de "My name is Y"para "My name is X".
abarnert

Respostas:

244

Primeiro, há realmente uma maneira muito menos invasiva. Tudo o que queremos fazer é mudar o que é printimpresso, certo?

_print = print
def print(*args, **kw):
    args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
            for arg in args)
    _print(*args, **kw)

Ou, da mesma forma, você pode monkeypatch em sys.stdoutvez de print.


Além disso, nada de errado com a exec … getsource …ideia. Bem, é claro que há muito errado com isso, mas menos do que se segue aqui…


Mas se você deseja modificar as constantes de código do objeto de função, podemos fazer isso.

Se você realmente quiser brincar com objetos de código de verdade, use uma biblioteca como bytecode(quando terminar) ou byteplay(até então, ou para versões mais antigas do Python) em vez de fazê-lo manualmente. Mesmo para algo tão trivial, o CodeTypeinicializador é uma dor; se você realmente precisa fazer coisas como consertar lnotab, apenas um lunático faria isso manualmente.

Além disso, é evidente que nem todas as implementações do Python usam objetos de código no estilo CPython. Esse código funcionará no CPython 3.7 e, provavelmente, todas as versões voltarão para pelo menos 2.2 com algumas pequenas alterações (e não as coisas de hackers de código, mas coisas como expressões de gerador), mas não funcionará com nenhuma versão do IronPython.

import types

def print_function():
    print ("This cat was scared.")

def main():
    # A function object is a wrapper around a code object, with
    # a bit of extra stuff like default values and closure cells.
    # See inspect module docs for more details.
    co = print_function.__code__
    # A code object is a wrapper around a string of bytecode, with a
    # whole bunch of extra stuff, including a list of constants used
    # by that bytecode. Again see inspect module docs. Anyway, inside
    # the bytecode for string (which you can read by typing
    # dis.dis(string) in your REPL), there's going to be an
    # instruction like LOAD_CONST 1 to load the string literal onto
    # the stack to pass to the print function, and that works by just
    # reading co.co_consts[1]. So, that's what we want to change.
    consts = tuple(c.replace("cat", "dog") if isinstance(c, str) else c
                   for c in co.co_consts)
    # Unfortunately, code objects are immutable, so we have to create
    # a new one, copying over everything except for co_consts, which
    # we'll replace. And the initializer has a zillion parameters.
    # Try help(types.CodeType) at the REPL to see the whole list.
    co = types.CodeType(
        co.co_argcount, co.co_kwonlyargcount, co.co_nlocals,
        co.co_stacksize, co.co_flags, co.co_code,
        consts, co.co_names, co.co_varnames, co.co_filename,
        co.co_name, co.co_firstlineno, co.co_lnotab,
        co.co_freevars, co.co_cellvars)
    print_function.__code__ = co
    print_function()

main()

O que poderia dar errado ao hackear objetos de código? Principalmente apenas segfaults, RuntimeErrors que consomem toda a pilha, RuntimeErrors mais normais que podem ser manipulados ou valores de lixo que provavelmente apenas aumentam um TypeErrorou AttributeErrorquando você tenta usá-los. Por exemplo, tente criar um objeto de código com apenas a RETURN_VALUEcom nada na pilha (bytecode b'S\0'para 3.6+ ou b'S'anterior) ou com uma tupla vazia para co_constsquando houver um LOAD_CONST 0no bytecode ou com varnamesdecrementado por 1 para que o mais alto LOAD_FASTrealmente carregue um freevar / célula cellvar. Para se divertir de verdade, se você lnotaberrar o suficiente, seu código será apenas falhado quando executado no depurador.

Usar bytecodeou byteplaynão protegê-lo de todos esses problemas, mas eles têm algumas verificações básicas de integridade e bons ajudantes que permitem que você insira um pedaço de código e se preocupe em atualizar todos os desvios e etiquetas para que você possa ' Não entendi errado, e assim por diante. (Além disso, eles impedem que você precise digitar o ridículo construtor de 6 linhas e depurar os erros de bobagem que surgem com isso.)


Agora vamos para o # 2.

Mencionei que os objetos de código são imutáveis. E é claro que os consts são uma tupla, então não podemos mudar isso diretamente. E a coisa na tupla constante é uma string, que também não podemos mudar diretamente. É por isso que eu tive que criar uma nova string para criar uma nova tupla para criar um novo objeto de código.

Mas e se você pudesse alterar uma string diretamente?

Bem, profundo o suficiente debaixo das cobertas, tudo é apenas um ponteiro para alguns dados C, certo? Se você estiver usando o CPython, existe uma API C para acessar os objetos , e você pode usá-la ctypespara acessar a API a partir do próprio Python, o que é uma péssima idéia que eles colocam pythonapiali no ctypesmódulo do stdlib . :) O truque mais importante que você precisa saber é que id(x)é o ponteiro real para a xmemória (como um int).

Infelizmente, a API C para seqüências de caracteres não nos permite acessar com segurança o armazenamento interno de uma sequência já congelada. Portanto, com segurança, vamos apenas ler os arquivos de cabeçalho e encontrar esse armazenamento.

Se você estiver usando o CPython 3.4 - 3.7 (é diferente para versões mais antigas e quem sabe o futuro), uma string literal de um módulo feito de ASCII puro será armazenada usando o formato ASCII compacto, que significa a estrutura termina cedo e o buffer de bytes ASCII segue imediatamente na memória. Isso quebrará (como provavelmente no segfault) se você colocar um caractere não ASCII na string ou certos tipos de strings não literais, mas você poderá ler as outras 4 maneiras de acessar o buffer para diferentes tipos de strings.

Para tornar as coisas um pouco mais fáceis, estou usando o superhackyinternalsprojeto no meu GitHub. (Não é intencionalmente instalável pelo pip, porque você realmente não deveria usá-lo, exceto para experimentar a compilação local do intérprete e assim por diante.)

import ctypes
import internals # https://github.com/abarnert/superhackyinternals/blob/master/internals.py

def print_function():
    print ("This cat was scared.")

def main():
    for c in print_function.__code__.co_consts:
        if isinstance(c, str):
            idx = c.find('cat')
            if idx != -1:
                # Too much to explain here; just guess and learn to
                # love the segfaults...
                p = internals.PyUnicodeObject.from_address(id(c))
                assert p.compact and p.ascii
                addr = id(c) + internals.PyUnicodeObject.utf8_length.offset
                buf = (ctypes.c_int8 * 3).from_address(addr + idx)
                buf[:3] = b'dog'

    print_function()

main()

Se você quiser brincar com essas coisas, inté muito mais simples do que isso str. E é muito mais fácil adivinhar o que você pode quebrar alterando o valor de 2para 1, certo? Na verdade, esqueça de imaginar, vamos fazê-lo (usando os tipos de superhackyinternalsnovo):

>>> n = 2
>>> pn = PyLongObject.from_address(id(n))
>>> pn.ob_digit[0]
2
>>> pn.ob_digit[0] = 1
>>> 2
1
>>> n * 3
3
>>> i = 10
>>> while i < 40:
...     i *= 2
...     print(i)
10
10
10

… Finja que a caixa de código tem uma barra de rolagem de comprimento infinito.

Tentei a mesma coisa no IPython e, na primeira vez em que tentei avaliar 2no prompt, ele entrou em algum tipo de loop infinito ininterrupto. Presumivelmente, ele está usando o número 2para algo em seu loop REPL, enquanto o intérprete de ações não está?

abarnert
fonte
11
@ cᴏʟᴅsᴘᴇᴇᴅ A codificação é indiscutivelmente razoável em Python, embora você geralmente queira tocar apenas em objetos de código por razões muito melhores (por exemplo, executar o bytecode por meio de um otimizador personalizado). Acessando o armazenamento interno de um PyUnicodeObject, de outro lado, isso é provavelmente realmente só Python no sentido de que um interpretador Python vai executá-lo ...
abarnert
4
Seu primeiro trecho de código é aumentado NameError: name 'arg' is not defined. Você quis dizer args = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]:? Uma maneira indiscutivelmente melhor para escrever este seria: args = [str(arg).replace('cat', 'dog') for arg in args]. Outro, ainda mais curto, a opção: args = map(lambda a: str(a).replace('cat', 'dog'), args). Isso tem o benefício adicional de argsser preguiçoso (o que também pode ser conseguido substituindo a compreensão da lista acima por uma geradora - *argsfunciona de qualquer maneira).
Konstantin
1
@ cᴏʟᴅs I'm Sim, IIRC, estou apenas usando a PyUnicodeObjectdefinição de struct, mas copiar isso para a resposta seria um obstáculo e acho que o leia-me e / ou os comentários da fonte para superhackyinternalsrealmente explicar como acessar o buffer (pelo menos bem o suficiente para me lembrar da próxima vez que me importo; não tenho certeza se será suficiente para mais alguém ...), que eu não queria entrar aqui. A parte relevante é como passar de um objeto Python ao vivo para sua PyObject *via ctypes. (E talvez simulando a aritmética de ponteiro, evitando automáticas char_pconversões, etc.)
abarnert
1
@ jpmc26 Acho que você não precisa fazer isso antes de importar módulos, desde que faça isso antes da impressão. Os módulos farão a pesquisa de nome sempre, a menos que sejam explicitamente vinculados printa um nome. Você também pode vincular o nome printpara eles: import yourmodule; yourmodule.print = badprint.
leewz
1
@ abarnert: Eu notei que você sempre avisou sobre isso (por exemplo, "você nunca quer realmente fazer isso" , "por que é uma má idéia alterar o valor" , etc.). Não está exatamente claro o que poderia dar errado (sarcasmo), você gostaria de elaborar um pouco sobre isso? Possivelmente, poderia ajudar aqueles tentados a tentar cegamente.
l'L'l
37

Patch de macaco print

printé uma função interna, portanto, ela usará a printfunção definida no builtinsmódulo (ou __builtin__no Python 2). Portanto, sempre que você desejar modificar ou alterar o comportamento de uma função interna, basta reatribuir o nome nesse módulo.

Este processo é chamado monkey-patching.

# Store the real print function in another variable otherwise
# it will be inaccessible after being modified.
_print = print  

# Actual implementation of the new print
def custom_print(*args, **options):
    _print('custom print called')
    _print(*args, **options)

# Change the print function globally
import builtins
builtins.print = custom_print

Depois disso, todas as printchamadas serão realizadas custom_print, mesmo que elas printestejam em um módulo externo.

No entanto, você realmente não deseja imprimir texto adicional, deseja alterar o texto impresso. Uma maneira de fazer isso é substituí-lo na string que seria impressa:

_print = print  

def custom_print(*args, **options):
    # Get the desired seperator or the default whitspace
    sep = options.pop('sep', ' ')
    # Create the final string
    printed_string = sep.join(args)
    # Modify the final string
    printed_string = printed_string.replace('cat', 'dog')
    # Call the default print function
    _print(printed_string, **options)

import builtins
builtins.print = custom_print

E, de fato, se você executar:

>>> def print_something():
...     print('This cat was scared.')
>>> print_something()
This dog was scared.

Ou se você escrever isso em um arquivo:

test_file.py

def print_something():
    print('This cat was scared.')

print_something()

e importe-o:

>>> import test_file
This dog was scared.
>>> test_file.print_something()
This dog was scared.

Então, ele realmente funciona como planejado.

No entanto, caso você queira temporariamente apenas imprimir um patch de macaco, você pode agrupá-lo em um gerente de contexto:

import builtins

class ChangePrint(object):
    def __init__(self):
        self.old_print = print

    def __enter__(self):
        def custom_print(*args, **options):
            # Get the desired seperator or the default whitspace
            sep = options.pop('sep', ' ')
            # Create the final string
            printed_string = sep.join(args)
            # Modify the final string
            printed_string = printed_string.replace('cat', 'dog')
            # Call the default print function
            self.old_print(printed_string, **options)

        builtins.print = custom_print

    def __exit__(self, *args, **kwargs):
        builtins.print = self.old_print

Portanto, quando você executa, isso depende do contexto impresso:

>>> with ChangePrint() as x:
...     test_file.print_something()
... 
This dog was scared.
>>> test_file.print_something()
This cat was scared.

Então é assim que você pode "hackear" printusando patches de macaco.

Modifique o destino em vez do print

Se você olhar para a assinatura, printnotará um fileargumento que é sys.stdoutpor padrão. Observe que esse é um argumento padrão dinâmico (ele realmente aparece sys.stdouttoda vez que você chama print) e não é um argumento padrão normal do Python. Portanto, se você alterar sys.stdout print, realmente imprimirá para o destino diferente, ainda mais conveniente que o Python também forneça uma redirect_stdoutfunção (do Python 3.4 em diante, mas é fácil criar uma função equivalente para as versões anteriores do Python).

A desvantagem é que não funcionará para printdeclarações que não são impressas sys.stdoute que a criação de suas próprias stdoutnão é realmente simples.

import io
import sys

class CustomStdout(object):
    def __init__(self, *args, **kwargs):
        self.current_stdout = sys.stdout

    def write(self, string):
        self.current_stdout.write(string.replace('cat', 'dog'))

No entanto, isso também funciona:

>>> import contextlib
>>> with contextlib.redirect_stdout(CustomStdout()):
...     test_file.print_something()
... 
This dog was scared.
>>> test_file.print_something()
This cat was scared.

Resumo

Alguns desses pontos já foram mencionados pelo @abarnet, mas eu queria explorar essas opções com mais detalhes. Especialmente como modificá-lo entre módulos (usando builtins/ __builtin__) e como fazer essa alteração apenas temporariamente (usando gerenciadores de contexto).

MSeifert
fonte
4
Sim, a coisa mais próxima dessa pergunta que alguém realmente deveria querer fazer é redirect_stdout, então é bom ter uma resposta clara que leve a isso.
abarnert
6

Uma maneira simples de capturar toda a saída de uma printfunção e processá-la é alterar o fluxo de saída para outra coisa, por exemplo, um arquivo.

Vou usar um PHPconvenções de nomenclatura ( ob_start , ob_get_contents , ...)

from functools import partial
output_buffer = None
print_orig = print
def ob_start(fname="print.txt"):
    global print
    global output_buffer
    print = partial(print_orig, file=output_buffer)
    output_buffer = open(fname, 'w')
def ob_end():
    global output_buffer
    close(output_buffer)
    print = print_orig
def ob_get_contents(fname="print.txt"):
    return open(fname, 'r').read()

Uso:

print ("Hi John")
ob_start()
print ("Hi John")
ob_end()
print (ob_get_contents().replace("Hi", "Bye"))

Imprimiria

Olá John Bye John

Uri Goren
fonte
5

Vamos combinar isso com a introspecção de quadros!

import sys

_print = print

def print(*args, **kw):
    frame = sys._getframe(1)
    _print(frame.f_code.co_name)
    _print(*args, **kw)

def greetly(name, greeting = "Hi")
    print(f"{greeting}, {name}!")

class Greeter:
    def __init__(self, greeting = "Hi"):
        self.greeting = greeting
    def greet(self, name):
        print(f"{self.greeting}, {name}!")

Você verá que esse truque antecede todas as saudações com a função ou método de chamada. Isso pode ser muito útil para registrar ou depurar; especialmente porque permite "seqüestrar" as instruções de impressão no código de terceiros.

Rafaël Dera
fonte