注:この質問は情報提供のみを目的としています。私は、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が説明したように、それはどのように行われるでしょうか。
第一に、実際にははるかに厄介な方法があります。私たちがしたいのは、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
、扱うことができるもっと普通のRuntimeError
s、あるいはあなたが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()
あなたがこのようなもので遊びたいのであれば、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
を最初に評価しようとしたとき、それはある種の中断のない無限ループに入りました。おそらく、それはREPLループ内の何かに2
という数字を使っていますが、株式インタプリタはそうではありませんか?
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.
あるいはそれをファイルに書いたとします。
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を使用)。
print
name__関数からすべての出力を取得して処理する簡単な方法は、出力ストリームを別のものに変更することです。ファイル。
PHP
name__という命名規則を使用します( 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}!")
あなたはこのトリックがすべての挨拶の前に呼び出し側の関数またはメソッドで始まることを見つけるでしょう。これはロギングやデバッグに非常に役立つかもしれません。特に、サードパーティコードで印刷ステートメントを「ハイジャック」することができます。