[python] 파이썬의 인쇄 기능을 “해킹”할 수 있습니까?

참고 :이 질문은 정보 제공만을위한 것입니다. 파이썬의 내부에 얼마나 깊은 지 알고 싶습니다.

얼마 전까지 만해도 인쇄 문에 전달 된 문자열을 호출 한 후 / 변경하는 동안 수정 될 수 있는지에 대한 특정 질문 에서 논의가 시작 print되었습니다. 예를 들어, 다음 기능을 고려하십시오.

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

이제 print실행되면 터미널에 대한 출력이 표시되어야합니다.

This dog was scared.

“cat”이라는 단어가 “dog”이라는 단어로 대체되었습니다. 어딘가에서 내부 버퍼를 수정하여 인쇄 된 내용을 변경할 수있었습니다. 이것이 원래 코드 작성자의 명시 적 허가없이 수행된다고 가정합니다 (따라서 해킹 / 도용).

특히 현명한 @abarnert 의이 의견 은 나를 생각하게했습니다.

이를 수행하는 데는 몇 가지 방법이 있지만 모두 매우 추악하므로 절대로 수행해서는 안됩니다. 가장 추악한 방법은 아마도 code함수 안의 객체를 다른 co_consts
목록을 가진 객체로 바꾸는 것입니다
. 다음은 아마도 str의 내부 버퍼에 액세스하기 위해 C API에 도달했을 것입니다. […]

그래서 이것이 실제로 가능한 것처럼 보입니다.

이 문제에 접근하는 순진한 방법은 다음과 같습니다.

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

물론, exec나쁘지만 실제로 호출 된 때 / 이후에 아무것도 수정하지 않기 때문에 실제로 질문에 대답하지 않습니다 print.

@abarnert가 설명했듯이 어떻게 수행됩니까?



답변

첫째, 실제로 해킹이 훨씬 덜합니다. 우리가하고 싶은 것은 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초기화 프로그램은 고통입니다. 실제로 lnotabfix up과 같은 작업을 수행 해야하는 경우 미치광이 만 수동으로 수행합니다.

또한 모든 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전체 스택을 먹는 s, RuntimeError처리 할 수있는 보다 정상적인 s TypeError또는 AttributeError사용하려고 할 때 가비지 값이 발생할 수 있습니다 . 예를 들어 RETURN_VALUE스택에 아무 ​​것도없는 코드 객체 ( b'S\0'3.6 + b'S'이전 바이트 코드) 또는 바이트 코드에 co_constsa가 있거나 빈 값이 1 인 경우 빈 튜플을 사용 LOAD_CONST 0하여 코드 객체를 만들어보십시오. / cellvar 셀. 약간의 재미를 위해, 충분히 잘못되면 코드가 디버거에서 실행될 때만 segfault가됩니다.varnamesLOAD_FASTlnotab

이러한 문제를 모두 사용 bytecode하거나 byteplay보호하지는 않지만 기본적인 위생 검사 및 코드 덩어리 삽입과 같은 작업을 수행하고 모든 오프셋 및 레이블 업데이트에 대해 걱정할 수있는 훌륭한 도우미가 있습니다. 잘못 이해하는 등. (또한, 그들은 당신이 그 우스운 6 줄 생성자를 타이핑하지 말고, 그렇게함으로써 어리석은 오타를 디버깅하지 않아도됩니다.)


이제 # 2로 넘어갑니다.

코드 객체는 변경할 수 없다고 언급했습니다. 물론 const는 튜플이므로 직접 변경할 수는 없습니다. 그리고 const 튜플의 것은 문자열이며, 우리는 직접 변경할 수도 없습니다. 그래서 새로운 코드 객체를 만들기 위해 새로운 튜플을 만들기 위해 새로운 문자열을 만들어야했습니다.

그러나 문자열을 직접 변경할 수 있다면 어떨까요?

글쎄, 엄밀히 살펴보면 모든 것이 C 데이터에 대한 포인터 일뿐입니다. 당신이 CPython의를 사용하는 경우, 거기 는 C API는 액세스 객체 , 그리고 당신이 사용할 수있는 ctypes접근이 그들이 넣어 그런 끔찍한 생각이다 파이썬 자체 내에서 API pythonapi다음 stdlib의 바로 거기 ctypes모듈 . 🙂 알아야 할 가장 중요한 트릭 은 메모리에서 id(x)실제 포인터입니다 x( int).

불행히도 문자열에 대한 C API를 사용하면 이미 고정 된 문자열의 내부 저장소를 안전하게 가져올 수 없습니다. 안전하게 조이 십시오. 헤더 파일을 읽고 그 저장소를 직접 찾으십시오.

CPython 3.4-3.7을 사용하는 경우 (이전 버전과 다르고 미래를 아는 사람) 순수 ASCII로 만들어진 모듈의 문자열 리터럴은 컴팩트 ASCII 형식을 사용하여 저장됩니다. 일찍 끝나고 ASCII 바이트 버퍼는 메모리에서 즉시 뒤 따릅니다. 문자열에 ASCII가 아닌 문자 또는 특정 종류의 문자가 아닌 문자열을 넣으면 (아마도 segfault에서와 같이) 중단되지만 다른 종류의 문자열에 대한 버퍼에 액세스하는 다른 4 가지 방법을 읽을 수 있습니다.

조금 더 쉽게하기 위해 superhackyinternalsGitHub 에서 프로젝트를 사용하고 있습니다. (이것은 인터프리터 등의 로컬 빌드를 실험하는 것을 제외하고는 이것을 사용하지 않아야하기 때문에 의도적으로 pip-installable이 아닙니다.)

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 루프에서 무언가에 숫자 를 사용하고 있습니까?


답변

원숭이 패치 print

print내장 함수이므로 모듈 (또는 Python 2)에 print정의 된 함수 를 사용합니다 . 따라서 내장 함수의 동작을 수정하거나 변경할 때마다 해당 모듈에서 단순히 이름을 재 할당 할 수 있습니다.builtins__builtin__

이 과정을이라고 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_printprint

그러나 실제로 추가 텍스트를 인쇄하고 싶지 않은 경우 인쇄되는 텍스트를 변경하려고합니다. 그것을 해결하는 한 가지 방법은 인쇄 할 문자열로 바꾸는 것입니다.

_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알 수 있습니다. 이것은 동적 기본 인수 ( 실제로 호출 할 때마다 조회 )이며 Python의 일반적인 기본 인수와는 다릅니다. 따라서 변경 하면 실제로 파이썬이 함수를 제공하는 것보다 훨씬 더 편리하게 다른 대상으로 인쇄 할 수 있습니다 (파이썬 3.4부터는 이전 파이썬 버전에 해당하는 함수를 쉽게 만들 수 있습니다).filesys.stdoutsys.stdoutprintsys.stdout printredirect_stdout

단점은 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__)과 임시로만 변경하는 방법 (컨텍스트 관리자 사용).


답변

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

인쇄 할 것이다

안녕 존 바이 존


답변

이것을 프레임 내부 검사와 결합합시다!

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

이 트릭은 호출 함수 또는 메소드로 모든 인사말을 시작합니다. 이것은 로깅 또는 디버깅에 매우 유용 할 수 있습니다. 특히 써드 파티 코드로 인쇄 문을 “도용”할 수 있기 때문입니다.


답변