Pythonの印刷機能を「ハッキング」することは可能ですか?


151

注:この質問は情報提供のみを目的としています。これでPythonの内部にどれだけ深く入ることができるのか興味があります。

少し前に、printステートメントに渡された文字列がへの呼び出しが行われた後/中に変更できるかどうかに関する特定の質問の中で議論が始まりましprintた。たとえば、次の関数について考えます。

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

これで、printが実行されると、ターミナルへの出力が表示されます。

This dog was scared.

「猫」という単語が「犬」という単語に置き換えられていることに注意してください。どこかで、何とかしてそれらの内部バッファーを変更して、出力内容を変更することができました。これは、元のコード作成者の明示的な許可なしに行われると想定します(したがって、ハッキング/ハイジャック)。

このコメント賢明@abarnertからは、具体的には、私が考えていました:

これを行うにはいくつかの方法がありますが、それらはすべて非常に醜く、決して行うべきではありません。最も醜い方法は、おそらくcode関数内のオブジェクトを別のco_consts リストを持つオブジェクトに置き換えること です。次はおそらく、C APIにアクセスしてstrの内部バッファにアクセスすることです。[...]

したがって、これは実際に可能であるように見えます。

この問題に取り組むための私の素朴な方法は次のとおりです。

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

もちろん、それexecは悪いことですが、それが実際に質問に答えることはありません。なぜなら、呼び出されたとき/後 printに呼び出されるときに実際には何も変更しないからです。

@abarnertが説明したように、それはどのように行われますか?


3
ちなみに、intの内部ストレージは文字列よりもはるかに単純で、float型の場合もそうです。それはの値を変更することは悪い考えである理由と、ボーナスとして、それは多くの明白だ4223、それはの値を変更するために悪い考えですなぜより"My name is Y"にします"My name is X"
abarnert 2018年

回答:


244

最初に、実際にはそれほどハックされない方法があります。私たちがやりたいことは、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()

コードオブジェクトのハッキングで何が問題になるのでしょうか?セグメンテーション違反、ほとんどがちょうどRuntimeErrorスタック全体を食べるの、より正常なRuntimeErrorおそらくちょうど引き上げる扱うことができ、S、またはごみ値TypeErrorまたはAttributeErrorあなたがそれらを使用しようとします。例RETURN_VALUEとして、スタックに何もない(b'S\0'3.6+ b'S'以前のバイトコード)、またはバイトコードにco_constsaがある場合は空のタプルを使用するLOAD_CONST 0か、varnames1 LOAD_FASTずつデクリメントしてコードオブジェクトを作成してみてください。/ cellvarセル。実際の楽しみとして、lnotab十分に間違えた場合、コードはデバッガーで実行されたときにのみsegfaultになります。

これらのすべての問題を使用するbytecodebyteplay、保護しませんが、いくつかの基本的な健全性チェックと、コードのチャンクの挿入などの操作を可能にする優れたヘルパーがあり、すべてのオフセットとラベルの更新を心配できるため、 t間違えるなど。(さらに、ばかげた6行のコンストラクターを入力したり、そうしたことによる愚かなタイプミスをデバッグしたりする必要がなくなります。)


次に#2に進みます。

コードオブジェクトは不変であると述べました。もちろん、constはタプルなので、直接変更することはできません。また、constタプルの内容は文字列であり、直接変更することもできません。新しいコードオブジェクトを作成するために、新しい文字列を作成して新しいタプルを作成する必要があったのはそのためです。

しかし、文字列を直接変更できるとしたらどうでしょう。

ええと、カバーの下で十分に深く、すべてはいくつかのCデータへの単なるポインタですよね?あなたはCPythonのを使用している場合は、がありますCのAPIは、オブジェクトがアクセスすると、あなたは使用することができctypes、彼らが置かれるように恐ろしい考えであるのPython自体、内部からのアクセスそのAPIにpythonapiSTDLIBの中で右がctypesモジュール。:)あなたが知る必要がある最も重要なトリックid(x)は、それがx(としてint)メモリ内の実際のポインタであることです。

残念ながら、文字列用のC APIでは、既に凍結された文字列の内部ストレージに安全にアクセスできません。安全にねじ込みますヘッダーファイル読んで、自分でそのストレージを見つけましょう。

CPython 3.4-3.7を使用している場合(古いバージョンとは異なり、将来的には誰が知っているか)、純粋なASCIIで作成されたモジュールからの文字列リテラルは、コンパクトなASCII形式を使用して格納されます。メモリ内ですぐに終了し、ASCIIバイトのバッファがすぐに続きます。これは(おそらくsegfaultのように)文字列に非ASCII文字を入れたり、特定の種類の非リテラル文字列を入れたりすると壊れますが、さまざまな種類の文字列のバッファにアクセスする他の4つの方法を読むことができます。

少し簡単にするために、私はsuperhackyinternalsGitHub以外のプロジェクトを使用しています。(インタープリターのローカルビルドなどを試す場合を除いて、これを実際に使用するべきではないため、意図的に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ループで何かの数を使用していますが、株式インタープリターはそうではありませんか?


11
@cᴏʟᴅsᴘᴇᴇᴅコードを変更することは間違いなく妥当なPythonですが、一般的にははるかに良い理由(たとえば、カスタムオプティマイザーを介してバイトコードを実行する)の場合にのみコードオブジェクトを操作します。PyUnicodeObject一方、の内部ストレージへのアクセスは、Pythonインタープリターがそれを実行するという意味で、おそらく実際にはPythonだけです...
abarnert

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 *via に取得する方法ctypesです。(そして、おそらくポインタ演算をシミュレートしたり、自動char_p変換を回避したりするなど)
abarnert

1
@ jpmc26 印刷する前であれば、モジュールをインポートするに行う必要はないと思います。モジュールは、明示的にprint名前にバインドしない限り、毎回名前の検索を行います。printそれらの名前をバインドすることもできますimport yourmodule; yourmodule.print = badprint
leewz 2018年

1
@abarnert:これを行うことについて頻繁に警告していることに気づきました(たとえば、実際にこれを実行したくない」「値を変更するのは悪い考えである理由」など)。何がうまくいかない可能性があるのか​​(皮肉)は明確ではありませんが、少し詳しく説明してもかまいませんか?それは、盲目的にそれを試してみたい誘惑に駆られた人たちを助けるかもしれません。
l'L'l

37

モンキーパッチ print

printは組み込み関数なのでprintbuiltinsモジュール(または__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_printprint

ただし、実際には追加のテキストを印刷する必要はなく、印刷されるテキストを変更する必要があります。これを実行する1つの方法は、出力される文字列でそれを置き換えることです。

_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の通常のデフォルト引数とは異なります。したがって、変更すると実際に別のターゲットに出力され、Pythonも関数を提供するのでさらに便利です(Python 3.4以降では、以前のバージョンのPythonと同等の関数を作成するのは簡単です)。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__)モジュール間でそれを変更する方法と、(contextmanagersを使用して)一時的にのみ変更する方法。


4
ええ、この質問に最も近いものは誰でも実際にしたいことredirect_stdoutなので、それにつながる明確な答えを持っているのは素晴らしいことです。
abarnert 2018年

6

print関数からのすべての出力をキャプチャしてから処理する簡単な方法は、出力ストリームを他の何か(ファイルなど)に変更することです。

PHP命名規則を使用します(ob_startob_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}!")

このトリックは、呼び出し元の関数またはメソッドでのすべての挨拶の前置きを見つけるでしょう。これは、ロギングやデバッグに非常に役立ちます。特に、サードパーティのコードで印刷ステートメントを「ハイジャック」できるためです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.