Во-первых, на самом деле есть гораздо менее хакерский способ. Все, что мы хотим сделать, это изменить то print
, что печатает, верно?
_print = print
def print(*args, **kw):
args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
for arg in args)
_print(*args, **kw)
Или, аналогично, вы можете sys.stdout
вместо monkeypatch print
.
Кроме того, нет ничего плохого в exec … getsource …
идее. Ну, конечно, с этим много чего не так, но здесь меньше того, что следует ...
Но если вы хотите изменить константы кода объекта функции, мы можем это сделать.
Если вы действительно хотите поэкспериментировать с объектами кода по-настоящему, вы должны использовать библиотеку, например bytecode
(когда она закончится) или byteplay
(до тех пор, или для более старых версий Python), вместо того, чтобы делать это вручную. Даже для чего-то такого тривиального CodeType
инициализатор - это боль; если вам действительно нужно что-то вроде ремонта lnotab
, то только сумасшедший сделает это вручную.
Кроме того, само собой разумеется, что не все реализации Python используют объекты кода в стиле CPython. Этот код будет работать в CPython 3.7, и, вероятно, все версии вернутся по крайней мере до 2.2 с некоторыми незначительными изменениями (и не с хакерскими кодами, а с такими вещами, как выражения генератора), но он не будет работать с любой версией 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()
Что может пойти не так со взломом объектов кода? В основном это просто ошибки segfaults, RuntimeError
которые поглощают весь стек, более обычные RuntimeError
s, которые можно обработать, или значения мусора, которые, вероятно, просто вызовут a TypeError
или AttributeError
когда вы попытаетесь их использовать. Например, попробуйте создать объект кода, в RETURN_VALUE
котором ничего не стоит в стеке (байт-код b'S\0'
для 3.6+, b'S'
ранее), или с пустым кортежем, co_consts
когда LOAD_CONST 0
в байт-коде есть a , или с varnames
уменьшенным на 1, так что наивысший LOAD_FAST
фактически загружает freevar. / Cellvar Cell. Для некоторого реального удовольствия, если вы поймете lnotab
неправильно, ваш код будет только segfault при запуске в отладчике.
Использование bytecode
или byteplay
не защитит вас от всех этих проблем, но у них есть некоторые базовые проверки работоспособности и хорошие помощники, которые позволяют вам делать такие вещи, как вставка фрагмента кода, и позволяют ему беспокоиться об обновлении всех смещений и меток, чтобы вы могли ' неправильно, и так далее. (Кроме того, вам не нужно вводить этот смешной 6-строчный конструктор и отлаживать глупые опечатки, возникающие при этом.)
Теперь перейдем к # 2.
Я упоминал, что объекты кода неизменны. И, конечно же, константы - это кортеж, поэтому мы не можем изменить это напрямую. И вещь в кортеже const - это строка, которую мы также не можем изменить напрямую. Вот почему я должен был создать новую строку, чтобы создать новый кортеж для создания нового объекта кода.
Но что, если бы вы могли изменить строку напрямую?
Ну, достаточно глубоко под прикрытием, все это просто указатель на некоторые данные C, верно? Если вы используете CPython, есть API C для доступа к объектам , и вы можете использовать его ctypes
для доступа к этому API из самого Python, что является настолько ужасной идеей, что они помещают ее pythonapi
прямо в ctypes
модуль stdlib . :) Самый важный трюк, который вам нужно знать, id(x)
это фактический указатель x
в памяти (как int
).
К сожалению, C API для строк не позволит нам безопасно попасть во внутреннее хранилище уже замороженной строки. Так что винт, давайте просто прочитаем заголовочные файлы и сами найдем это хранилище.
Если вы используете CPython 3.4 - 3.7 (он отличается от старых версий и знает, что будет в будущем), строковый литерал из модуля, который состоит из чистого ASCII, будет храниться в компактном формате ASCII, что означает структуру заканчивается рано, и буфер байтов ASCII немедленно следует в памяти. Это сломается (как, вероятно, в segfault), если вы поместите не-ASCII-символ в строку или некоторые виды не-литеральных строк, но вы можете прочитать другие 4 способа доступа к буферу для различных типов строк.
Чтобы упростить задачу, я использую superhackyinternals
проект с моего GitHub. (Это намеренно не устанавливается в pip, потому что вы действительно не должны использовать это, кроме как для экспериментов с вашей локальной сборкой интерпретатора и т.п.).
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()
Если вы хотите поиграть с этим, int
намного проще под покровом, чем str
. И гораздо проще угадать, что можно сломать, изменив значение 2
на 1
, верно? На самом деле, забудьте о воображении, давайте просто сделаем это (используя типы superhackyinternals
снова):
>>> 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
... притворимся, что кодовое поле имеет полосу прокрутки бесконечной длины.
Я попробовал то же самое в IPython, и в первый раз, когда я попытался оценить 2
в приглашении, он вошел в какой-то непрерывный бесконечный цикл. Предположительно он использует число 2
для чего-то в цикле REPL, в то время как стандартный интерпретатор - нет?
42
на,23
чем плохая идея изменить значение"My name is Y"
на"My name is X"
.