web-dev-qa-db-ja.com

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

注:この質問は情報提供のみを目的としています。私は、Pythonの内部構造のどれだけこれがうまくいくのかを知りたいと思っています。

それほど昔ではないが、print文に渡された文字列がprintへの呼び出しの後または途中で変更される可能性があるかどうかに関して、ある 質問 の中で議論が始まった。製。たとえば、次の関数を考えてください。

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

printが実行されると、端末への出力は次のようになります。

This dog was scared.

単語 "cat"が単語 "dog"に置き換えられていることに注意してください。どこかで何かがどういうわけか印刷されたものを変えるためにそれらの内部バッファを修正することができました。これは元のコード作成者の明示的な許可なしに行われると仮定します(したがって、ハッキング/ハイジャック)。

特に賢い@abarnertからのこの コメント は、私に考えさせました:

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

それで、これは実際に可能であるように見えます。

これがこの問題に取り組む私の素朴な方法です。

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

もちろん、execはまずいですが、when/afterprintが呼び出されても実際には何も変更されないため、実際には質問に答えていません。

@abarnertが説明したように、それはどのように行われるでしょうか。

146
cs95

第一に、実際にははるかに厄介な方法があります。私たちがしたいのは、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)

あるいは、同様に、printの代わりにsys.stdoutをmonkeypatchすることもできます。


また、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、扱うことができるもっと普通のRuntimeErrors、あるいはあなたがTypeErrorまたはAttributeErrorを上げるだけのゴミ値それらを使ってみてください。例えば、スタック上に何もないRETURN_VALUE(3.6の場合はb'S\0'b'S'の場合)、バイトコードにco_constsがある場合はLOAD_CONST 0の空のTuple、またはvarnamesを使用してコードオブジェクトを作成してみてください。最も大きいLOAD_FASTが実際にfreevar/cellvarセルをロードするように、1ずつ減少します。ちょっとした楽しみのために、lnotabが十分に間違っていると、デバッガで実行したときにコードがセグメンテーション違反になります。

bytecodeまたはbyteplayを使用しても、これらの問題すべてからあなたを保護することはできませんが、基本的な健全性チェック、およびコードのチャンクを挿入して心配させるNiceヘルパーがあります。あなたがそれを誤解することができないようにすべてのオフセットとラベルを更新することについて、等々。 (プラス、彼らはあなたがそのばかげた6行のコンストラクタをタイプしなければならなくて、そしてそうすることから来る愚かなタイプミスをデバッグしなければならないことからあなたを守ります。)


今度は#2に。

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

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

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

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

もしあなたがCPython 3.4 - 3.7を使っているなら(それは古いバージョンでは異なり、将来は知っています)、純粋なASCIIからなるモジュールからの文字列リテラルはcompact _を使って格納されますASCIIフォーマット。これは、構造体が早く終了し、ASCIIバイトのバッファがメモリ内で直後に続くことを意味します。文字列にASCII以外の文字、またはある種の非リテラル文字列を入れると、これは(おそらくsegfaultのように)壊れますが、異なる種類の文字列についてバッファにアクセスする他の4つの方法で調べることができます。

作業を少し簡単にするために、私はGitHubの superhackyinternals プロジェクトを使用しています。 (インタープリタのローカルビルドなどを試す以外は、実際には使用しないでください。意図的に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()

あなたがこのようなもので遊びたいのであれば、intstrよりもはるかに簡単です。 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を最初に評価しようとしたとき、それはある種の中断のない無限ループに入りました。おそらく、それはREPLループ内の何かに2という数字を使っていますが、株式インタプリタはそうではありませんか?

235
abarnert

猿パッチprint

printは組み込み関数なので、printモジュール(またはPython 2の__builtin__)で定義されているbuiltins関数を使用します。そのため、組み込み関数の動作を変更または変更したいときはいつでも、そのモジュール内の名前を再割り当てするだけで済みます。

このプロセスは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が外部モジュール内にあっても、すべてのprint呼び出しはcustom_printを通過します。

しかし、あなたは本当に追加のテキストを印刷したくない、あなたは印刷されるテキストを変更したいのです。そのための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 のシグネチャを見ると、デフォルトでsys.stdoutであるfile引数があります。これは動的なデフォルトの引数であり(printを呼び出すたびにsys.stdoutを検索する)(it、実際にはであり、Pythonの通常のデフォルト引数とは異なります。そのため、sys.stdoutを変更した場合、printは実際にはさらに便利な別のターゲットに出力されるので、Pythonは redirect_stdout 関数も提供します(Python 3.4以降)が、簡単です。以前のPythonバージョンと同等の関数を作成するため)。

欠点は、sys.stdoutに出力されないprintステートメントには機能しないこと、そして独自の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__を使用)およびその変更を一時的なものにする方法(contextmanagerを使用)。

34
MSeifert

printname__関数からすべての出力を取得して処理する簡単な方法は、出力ストリームを別のものに変更することです。ファイル。

PHPname__という命名規則を使用します( 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"))

印刷しますか

こんにちはジョンバイジョン

6
Uri Goren

これをフレームイントロスペクションと組み合わせてみましょう。

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

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

4
Rafaël Dera