Czy można „zhakować” funkcję drukowania Pythona?

151

Uwaga: to pytanie służy wyłącznie celom informacyjnym. Interesuje mnie, jak głęboko w wewnętrzne elementy Pythona można się z tym pogodzić.

Nie tak dawno temu rozpoczęła się dyskusja w ramach pewnego pytania dotyczącego tego, czy łańcuchy przekazane do instrukcji print mogą być modyfikowane po / w trakcie wywołania funkcji print. Na przykład rozważmy funkcję:

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

Teraz, kiedy printjest uruchomiony, wyjście do terminala powinno wyświetlić:

This dog was scared.

Zwróć uwagę, że słowo „kot” zostało zastąpione słowem „pies”. Coś gdzieś było w stanie zmodyfikować te wewnętrzne bufory, aby zmienić to, co zostało wydrukowane. Załóżmy, że dzieje się to bez wyraźnej zgody autora oryginalnego kodu (stąd włamanie / przejęcie).

Szczególnie ten komentarz mądrego @abarnert skłonił mnie do myślenia:

Jest na to kilka sposobów, ale wszystkie są bardzo brzydkie i nigdy nie powinno się ich robić. Najmniej brzydkim sposobem jest prawdopodobnie zastąpienie codeobiektu wewnątrz funkcji obiektem z inną co_consts listą. Następnym krokiem jest prawdopodobnie sięgnięcie do C API w celu uzyskania dostępu do wewnętrznego bufora str. […]

Wygląda więc na to, że jest to rzeczywiście możliwe.

Oto mój naiwny sposób podejścia do tego problemu:

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

Oczywiście execjest źle, ale to tak naprawdę nie odpowiada na pytanie, ponieważ tak naprawdę nie modyfikuje niczego podczas print wywołania / po .

Jak by to zrobić, jak wyjaśnił @abarnert?

cs95
źródło
3
Nawiasem mówiąc, pamięć wewnętrzna dla int jest o wiele prostsza niż stringi, a jeszcze bardziej unosi się. A jako bonus, to dużo bardziej oczywiste, dlaczego jest to zły pomysł, aby zmienić wartość 42na 23ponad dlaczego jest to zły pomysł, aby zmienić wartość "My name is Y"na "My name is X".
abarnert

Odpowiedzi:

243

Po pierwsze, w rzeczywistości istnieje znacznie mniej hakerski sposób. Chcemy tylko zmienić jakie printwydruki, prawda?

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

Lub, podobnie, sys.stdoutzamiast tego możesz użyć monkeypatch print.


Nie ma też nic złego w exec … getsource …pomyśle. Cóż, oczywiście jest w tym wiele nie tak, ale mniej niż to, co następuje tutaj…


Ale jeśli chcesz zmodyfikować stałe kodu obiektu funkcji, możemy to zrobić.

Jeśli naprawdę chcesz naprawdę bawić się obiektami kodu, powinieneś użyć biblioteki takiej jak bytecode(po zakończeniu) lub byteplay(do tego czasu lub dla starszych wersji Pythona) zamiast robić to ręcznie. Nawet w przypadku czegoś tak trywialnego CodeTypeinicjalizacja jest uciążliwa; jeśli naprawdę musisz zrobić coś takiego jak naprawianie lnotab, tylko szaleniec zrobiłby to ręcznie.

Ponadto jest oczywiste, że nie wszystkie implementacje Pythona używają obiektów kodu w stylu CPythona. Ten kod będzie działał w CPythonie 3.7 i prawdopodobnie we wszystkich wersjach co najmniej 2.2 z kilkoma drobnymi zmianami (nie dotyczy to hakowania kodu, ale rzeczy takie jak wyrażenia generatora), ale nie będzie działać z żadną wersją 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()

Co może pójść nie tak z hakowaniem obiektów kodu? Przeważnie to zwykłe segfaulty, RuntimeErrorktóre pochłaniają cały stack, bardziej normalne, RuntimeErrorktóre można obsłużyć, lub śmieciowe wartości, które prawdopodobnie po prostu podniosą a TypeErrorlub AttributeErrorgdy spróbujesz ich użyć. Na przykład spróbuj utworzyć obiekt kodu zawierający tylko znak RETURN_VALUEz niczym na stosie (kod bajtowy b'S\0'dla 3.6+, b'S'wcześniej) lub z pustą krotką, co_constsgdy w kodzie bajtowym znajduje się znak LOAD_CONST 0lub z varnamesdekrementacją o 1, aby najwyższy LOAD_FASTfaktycznie ładował freevar / cellvar cell. Dla prawdziwej zabawy, jeśli lnotabpomylisz się wystarczająco, twój kod będzie segfaultowany tylko wtedy, gdy zostanie uruchomiony w debugerze.

Używanie bytecodelub byteplaynie ochroni Cię przed wszystkimi tymi problemami, ale mają kilka podstawowych testów poczytalności i fajnych pomocników, które pozwalają ci zrobić takie rzeczy, jak wstawienie kawałka kodu i niech martwi się o aktualizację wszystkich przesunięć i etykiet, abyś mógł '' nie zrozumiem tego źle i tak dalej. (Poza tym nie musisz wpisywać tego śmiesznego 6-liniowego konstruktora i debugować głupie literówki, które z tego wynikają).


Teraz przejdźmy do # 2.

Wspomniałem, że obiekty kodu są niezmienne. Oczywiście stałe są krotką, więc nie możemy tego bezpośrednio zmienić. A rzeczą w stałej krotce jest łańcuch, którego również nie możemy bezpośrednio zmienić. Dlatego musiałem zbudować nowy ciąg, aby zbudować nową krotkę i zbudować nowy obiekt kodu.

Ale co by było, gdybyś mógł bezpośrednio zmienić ciąg?

Cóż, wystarczająco głęboko pod kołdrą, wszystko jest tylko wskaźnikiem do niektórych danych w C, prawda? Jeśli używasz CPythona, istnieje C API, aby uzyskać dostęp do obiektów , i możesz go użyć, ctypesaby uzyskać dostęp do tego API z samego Pythona, co jest tak okropnym pomysłem, że umieścili pythonapitam bezpośrednio w ctypesmodule stdlib . :) Najważniejszą sztuczką, którą musisz wiedzieć, id(x)jest faktyczny wskaźnik xw pamięci (jako int).

Niestety, C API dla stringów nie pozwala nam bezpiecznie dostać się do wewnętrznej pamięci już zamrożonego łańcucha. Więc chrzanić bezpiecznie, po prostu przeczytajmy pliki nagłówkowe i sami znajdźmy to miejsce .

Jeśli używasz CPython 3.4 - 3.7 (jest inny dla starszych wersji i kto wie na przyszłość), literał ciągu z modułu, który jest wykonany z czystego ASCII, będzie przechowywany w kompaktowym formacie ASCII, co oznacza, że ​​struktura kończy się wcześniej, a bufor bajtów ASCII następuje natychmiast w pamięci. To się zepsuje (jak w prawdopodobnie segfault), jeśli umieścisz w ciągu znak inny niż ASCII lub pewne rodzaje nieliteralnych ciągów, ale możesz przeczytać pozostałe 4 sposoby dostępu do bufora dla różnych rodzajów ciągów.

Aby trochę ułatwić, używam superhackyinternalsprojektu poza moim GitHubem. (Celowo nie można go zainstalować za pomocą pip, ponieważ naprawdę nie powinieneś go używać, z wyjątkiem eksperymentowania z lokalną kompilacją interpretera i tym podobnymi).

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

Jeśli chcesz się tym bawić, pod kołdrą intjest o wiele prostsze niż str. O wiele łatwiej jest zgadnąć, co można złamać, zmieniając wartość 2na 1, prawda? Właściwie zapomnij o wyobrażeniach, po prostu zróbmy to (używając superhackyinternalsponownie typów z ):

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

… Udawaj, że skrzynka z kodem ma pasek przewijania o nieskończonej długości.

Wypróbowałem to samo w IPythonie i kiedy pierwszy raz spróbowałem ocenić 2w zachęcie, wszedł on w jakąś nieprzerwaną nieskończoną pętlę. Prawdopodobnie używa numeru 2do czegoś w swojej pętli REPL, podczas gdy interpreter zapasów nie?

abarnert
źródło
11
@ cᴏʟᴅsᴘᴇᴇᴅ Munging kodu jest prawdopodobnie rozsądnym Pythonem, chociaż generalnie chcesz dotykać obiektów kodu tylko z dużo lepszych powodów (np. uruchamianie kodu bajtowego przez niestandardowy optymalizator). PyUnicodeObjectZ drugiej strony, dostęp do pamięci wewnętrznej a , to prawdopodobnie tak naprawdę tylko Python w tym sensie, że interpreter Pythona będzie go uruchamiał…
abarnert
4
Twój pierwszy fragment kodu podnosi NameError: name 'arg' is not defined. Czy chodziło Ci o args = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]? Zapewne lepszy sposób napisać to będzie: args = [str(arg).replace('cat', 'dog') for arg in args]. Innym, jeszcze krócej, opcjonalnie: args = map(lambda a: str(a).replace('cat', 'dog'), args). Ma to dodatkową zaletę, że argsjest leniwy (co można również osiągnąć poprzez zastąpienie powyższego rozumienia list z generatorem - *argsdziała w obu przypadkach).
Konstantin
1
@ cᴏʟᴅsᴘᴇᴇᴅ Tak, IIRC Używam tylko PyUnicodeObjectdefinicji struktury, ale skopiowanie jej do odpowiedzi mogłoby po prostu przeszkodzić i myślę, że komentarze readme i / lub źródła superhackyinternalswyjaśniają, jak uzyskać dostęp do bufora (przynajmniej wystarczająco dobrze, aby przypomnieć mi następnym razem, gdy mi zależy; nie jestem pewien, czy wystarczy to komukolwiek innemu…), o czym nie chciałem się tutaj dostać. Istotna część dotyczy tego, jak przejść z aktywnego obiektu Pythona do jego PyObject *via ctypes. (I może symulując arytmetykę wskaźnika, unikając automatycznych char_pkonwersji itp.)
abarnert
1
@ jpmc26 Myślę, że nie musisz tego robić przed zaimportowaniem modułów, o ile robisz to przed wydrukowaniem. Moduły będą wyszukiwać nazwę za każdym razem, chyba że jawnie się printz nią połączą. Można także powiązać nazwę printdla nich import yourmodule; yourmodule.print = badprint.
Leewz
1
@abarnert: Zauważyłem, że często ostrzegałeś przed tym (np. „nigdy nie chcesz tego robić” , „dlaczego zmiana wartości jest złym pomysłem” itp.). Nie jest do końca jasne, co może się nie udać (sarkazm), czy byłbyś skłonny trochę się nad tym rozwinąć? Może to pomóc tym, którzy mają ochotę spróbować na ślepo.
l'L'l
37

Łatka małpy print

printjest funkcją wbudowaną, więc użyje printfunkcji zdefiniowanej w builtinsmodule (lub __builtin__w Pythonie 2). Więc za każdym razem, gdy chcesz zmodyfikować lub zmienić zachowanie funkcji wbudowanej, możesz po prostu ponownie przypisać nazwę w tym module.

Ten proces nazywa się 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

Po tym każde printpołączenie będzie przekazywane custom_print, nawet jeśli printjest w module zewnętrznym.

Jednak tak naprawdę nie chcesz drukować dodatkowego tekstu, chcesz zmienić drukowany tekst. Jednym ze sposobów jest zastąpienie go w ciągu, który zostałby wydrukowany:

_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

I rzeczywiście, jeśli biegniesz:

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

Lub jeśli zapiszesz to do pliku:

plik_testowy.py

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

print_something()

i zaimportuj:

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

Więc to naprawdę działa zgodnie z przeznaczeniem.

Jednak w przypadku, gdy chcesz tylko tymczasowo drukować małpy, możesz umieścić to w menedżerze kontekstu:

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

Więc po uruchomieniu zależy to od kontekstu, co jest drukowane:

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

Więc w ten sposób można "hakować" printprzez małpowanie.

Zmodyfikuj cel zamiast print

Jeśli spojrzysz na podpis print, zauważysz fileargument, który jest sys.stdoutdomyślny. Zauważ, że jest to dynamiczny argument domyślny ( naprawdę wygląda w górę za sys.stdoutkażdym razem, gdy dzwonisz print), a nie jak zwykłe domyślne argumenty w Pythonie. Więc jeśli zmienisz sys.stdout print, faktycznie wydrukujesz do innego celu, jeszcze wygodniej, że Python również zapewnia redirect_stdoutfunkcję (od Pythona 3.4, ale łatwo jest utworzyć równoważną funkcję dla wcześniejszych wersji Pythona).

Wadą jest to, że nie zadziała w przypadku printinstrukcji, które nie są drukowane, sys.stdouta tworzenie własnych stdoutnie jest naprawdę proste.

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

Jednak działa to również:

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

Podsumowanie

@Abarnet wspomniał już o niektórych z tych punktów, ale chciałem zbadać te opcje bardziej szczegółowo. Zwłaszcza jak zmodyfikować to w modułach (używając builtins/ __builtin__) i jak uczynić tę zmianę tylko tymczasową (używając menedżerów kontekstu).

MSeifert
źródło
4
Tak, najbliższą rzeczą na to pytanie, które ktokolwiek powinien kiedykolwiek chcieć, jest redirect_stdout, więc miło jest mieć jasną odpowiedź, która do tego prowadzi.
abarnert
6

Prostym sposobem na przechwycenie całego wyjścia printfunkcji, a następnie jego przetworzenie, jest zmiana strumienia wyjściowego na coś innego, np. Plik.

Użyję PHPkonwencje nazewnictwa ( 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()

Stosowanie:

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

Wydrukowałoby

Cześć John Cześć John

Uri Goren
źródło
5

Połączmy to z introspekcją ramek!

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

Przekonasz się, że ta sztuczka poprzedza każde powitanie funkcją lub metodą wywołującą. Może to być bardzo przydatne do logowania lub debugowania; zwłaszcza, że ​​pozwala "przechwytywać" instrukcje drukowania w kodzie strony trzeciej.

Rafaël Dera
źródło