По-перше, насправді існує набагато менш хакітний спосіб. Все, що ми хочемо зробити, - це змінити 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замість маніпулювання 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()
Що може піти не так у злому об'єктів коду? Здебільшого це лише segfault, RuntimeErrorякі з’їдають цілий стек, більш нормальні RuntimeErrors, з якими можна обробляти, або значення сміття, які, ймовірно, просто піднімають a TypeErrorабо AttributeErrorпри спробі їх використовувати. Наприклад, спробуйте створити об'єкт коду просто RETURN_VALUEзі b'S\0'значком на стеку (байт-код для 3.6+ b'S'раніше) або з порожнім кортежем для того, co_constsколи є LOAD_CONST 0байт-код, або з varnamesзменшеним на 1, тому найвищий LOAD_FASTнасправді завантажує фрівар / клітина клітин. Для справжньої розваги, якщо ви отримаєте lnotabдосить неправильну, ваш код буде лише за замовчуванням під час запуску в налагоджувачі.
Використання bytecodeабо byteplayне захистить вас від усіх цих проблем, але у них є деякі основні перевірки на корисність та приємні помічники, які дозволяють вам робити такі речі, як вставити шматок коду і дозволяти турбуватися про оновлення всіх компенсацій та міток, щоб ви могли " t неправильно і так далі. (Крім того, вони не дозволяють вам набирати цей смішний 6-лінійний конструктор і не вимагати налагодження дурних друкарських помилок, які випливають із цього.)
Тепер до №2.
Я згадав, що об'єкти коду незмінні. І, звичайно, конкурси є кортежем, тому ми не можемо змінити це безпосередньо. А річ у const кортежі - це струна, яку ми також не можемо безпосередньо змінити. Ось чому мені довелося побудувати новий рядок, щоб побудувати новий кортеж, щоб побудувати новий об'єкт коду.
Але що робити, якщо ви могли змінити рядок безпосередньо?
Ну, досить глибоко під обкладинками, все лише вказівник на деякі дані С, правда? Якщо ви використовуєте CPython, для доступу до об'єктів є API C , і ви можете використовувати його ctypesдля доступу до цього API зсередини самого Python, що є такою жахливою ідеєю, що вони розміщують pythonapiтут же ctypesмодуль stdlib . :) Найголовніший трюк, який потрібно знати, це те, що id(x)є фактичним вказівником на xпам'ять (як на int).
На жаль, API C для рядків не дозволить нам спокійно потрапити на внутрішнє зберігання вже замороженої рядки. Тож накрутіть сміливо, давайте просто прочитаємо файли заголовків і самі знайдемо це сховище.
Якщо ви використовуєте CPython 3.4 - 3.7 (це відрізняється від старих версій, і хто знає на майбутнє), рядковий літерал з модуля, виготовлений із чистого ASCII, буде зберігатися у компактному форматі ASCII, що означає структуру закінчується рано, і в пам'яті негайно випливає буфер байтів ASCII. Це порушиться (як, напевно, у сегментах), якщо ви додасте символ не ASCII у рядок або певні види нелітеральних рядків, але ви можете прочитати інші 4 способи доступу до буфера для різних типів рядків.
Щоб зробити щось легше, я використовую superhackyinternalsпроект від мого GitHub. .
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".