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á?
42para do23que por que é uma má idéia alterar o valor de"My name is Y"para"My name is X".