Чи можна "зламати" функцію друку Python?


151

Примітка. Це запитання призначене лише для ознайомлення. Мені цікаво побачити, наскільки глибоко у внутрішніх умовах Python можна піти з цим.

Не так давно почалося обговорення у певному питанні щодо того, чи можна рядки, передані для друку заяв, змінюватись після / під час виклику print. Наприклад, розглянемо функцію:

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

Тепер, коли printзапущено, то на виході до терміналу повинен відображатися:

This dog was scared.

Зауважте, слово "кішка" замінено словом "собака". Щось десь якось вдалося змінити ті внутрішні буфери, щоб змінити те, що було надруковано. Припустимо, це робиться без явного дозволу автора оригінального коду (отже, злому / викрадення).

Цей коментар від мудрого @abarnert, зокрема, змусив мене задуматися:

Є кілька способів зробити це, але всі вони дуже некрасиві, і цього ніколи не слід робити. Найменш негарний спосіб - це, ймовірно, замінити codeоб'єкт всередині функції на інший co_consts список. Далі, ймовірно, потрапляє в API C для доступу до внутрішнього буфера str. [...]

Отже, схоже, що це насправді можливо.

Ось мій наївний спосіб підходу до цієї проблеми:

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

Звичайно, execце погано, але це насправді не відповідає на питання, оскільки воно насправді нічого не змінює під час виклику / після print .

Як би це було зроблено, як @abarnert пояснив це?


3
До речі, внутрішнє зберігання для ints набагато простіше, ніж рядки, і плаває тим більше. І, в якості бонусу, це набагато більш зрозуміло , чому це погана ідея , щоб змінити значення 42в 23ніж чому це погана ідея , щоб змінити значення "My name is Y"для "My name is X".
abarnert

Відповіді:


243

По-перше, насправді існує набагато менш хакітний спосіб. Все, що ми хочемо зробити, - це змінити 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, тоді як перекладач запасів не є?


11
@ cᴏʟᴅsᴘᴇᴇᴅ Зміна коду є, напевно, розумним Python, хоча ви, як правило, бажаєте торкатися кодових об'єктів лише з набагато кращих причин (наприклад, запуск байтового коду через спеціальний оптимізатор). PyUnicodeObjectЗ іншого боку, доступ до внутрішнього сховища
файлу

4
Ваш перший фрагмент коду підвищується NameError: name 'arg' is not defined. Можливо , ви мали в виду: args = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]? , Можливо , найкращий спосіб , щоб написати це було б: args = [str(arg).replace('cat', 'dog') for arg in args]. Інший, ще коротше, варіант: args = map(lambda a: str(a).replace('cat', 'dog'), args). Це має додаткову перевагу, яка argsледача (що також може бути досягнуто заміною вищезгаданого списку на генератор - *argsпрацює в будь-якому випадку).
Костянтин

1
@ cᴏʟᴅsᴘᴇᴇᴅ Так, IIRC, я використовую лише PyUnicodeObjectвизначення структури, але, копіюючи це у відповідь, я думаю, що просто перешкоджаю, і я думаю, що readme та / або коментарі джерела superhackyinternalsнасправді пояснюють, як отримати доступ до буфера (принаймні достатньо добре, щоб нагадати мені наступного разу, коли я дбаю; не впевнений, чи цього вистачить комусь іншому ...), на який я не хотів потрапляти сюди. Відповідна частина полягає в тому, як дістатися з живого об’єкта Python до його PyObject *через ctypes. (І можливо, імітуючи арифметику вказівника, уникаючи автоматичних char_pперетворень тощо)
abarnert

1
@ jpmc26 Я не думаю, що вам потрібно робити це перед імпортом модулів, якщо ви робите це перед тим, як надрукувати. Модулі здійснюватимуть пошук імен кожного разу, якщо вони явно не прив'язуються printдо імені. Ви також можете пов'язати ім'я printдля них: import yourmodule; yourmodule.print = badprint.
leewz

1
@abarnert: Я помічав, що ви часто попереджали про це (наприклад, "ви ніколи не хочете цього робити" , "чому це погана ідея змінювати значення" тощо). Не зовсім зрозуміло, що могло піти не так (сарказм), чи готові ви трохи розібратися в цьому? Можливо, це може допомогти тим, хто спокусить сліпо спробувати.
l'L'l

37

Мавпа-латка print

printє вбудованою функцією, тому вона буде використовувати printфункцію, визначену в builtinsмодулі (або __builtin__в Python 2). Отже, коли ви хочете змінити або змінити поведінку вбудованої функції, ви можете просто присвоїти ім'я в цьому модулі.

Цей процес називається 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

Після цього кожен printдзвінок буде проходити custom_print, навіть якщо він printзнаходиться у зовнішньому модулі.

Однак ви не хочете надрукувати додатковий текст, ви хочете змінити друкований текст. Один із способів зробити це - замінити його в рядку, який буде надрукований:

_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

І справді, якщо ти працюєш:

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

Або якщо ви пишете це у файл:

test_file.py

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

print_something()

та імпортуйте його:

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

Тож воно справді працює за призначенням.

Однак, якщо ви лише тимчасово хочете зробити маніпуляційний патч-друк, ви можете зафіксувати це в контекстному менеджері:

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

Тож коли ви запускаєте, це залежить від контексту, що надруковано:

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

Так ось як можна було "зламати" printмавпочку.

Змініть ціль замість print

Якщо ви подивитеся на підпис, printви помітите fileаргумент, який sys.stdoutза замовчуванням. Зауважте, що це динамічний аргумент за замовчуванням (він дійсно шукає sys.stdoutщоразу, коли ви телефонуєте print), а не як звичайні аргументи за замовчуванням у Python. Тож якщо ви зміните sys.stdout print, насправді буде надруковано на іншу ціль ще зручніше, що Python також надає redirect_stdoutфункцію (від Python 3.4 далі, але легко створити еквівалентну функцію для попередніх версій Python).

Мінус у тому, що він не працюватиме для printтверджень, які не друкуються, sys.stdoutі створення власних stdoutсправді не є простим.

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'))

Однак це також працює:

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

Підсумок

Деякі з цих моментів вже згадував @abarnet, але я хотів детальніше вивчити ці варіанти. Особливо, як змінити його через модулі (використовуючи builtins/ __builtin__) та як зробити цю зміну лише тимчасовою (за допомогою контекстних менеджерів).


4
Так, найближче до цього питання, кого б хто-небудь насправді хотів зробити, - redirect_stdoutце приємно мати чітку відповідь, яка веде до цього.
abarnert

6

Простий спосіб зафіксувати весь вихід з printфункції та потім обробити її - це змінити вихідний потік на щось інше, наприклад, файл.

Я буду використовувати умови PHPіменування ( 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()

Використання:

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

Друкував би

Привіт, Джон Бай Джон


5

Давайте поєднаємо це з інтроспекцією кадру!

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}!")

Ви знайдете цей фокус передмовою кожного привітання з функцією або методом виклику. Це може бути дуже корисно для ведення журналів або налагодження; тим більше, що це дозволяє "викрасти" друковані виписки у коді сторонніх розробників.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.