İlk olarak, aslında çok daha az hileli bir yol var. Tüm yapmak istediğimiz print
baskıları değiştirmek değil mi?
_print = print
def print(*args, **kw):
args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
for arg in args)
_print(*args, **kw)
Ya da benzer şekilde, sys.stdout
bunun yerine monkeypatch yapabilirsiniz print
.
Ayrıca, exec … getsource …
fikirde yanlış bir şey yok . Tabii ki, bununla ilgili çok yanlış var, ama burada takip edenlerden daha az…
Ancak, işlev nesnesinin kod sabitlerini değiştirmek isterseniz, bunu yapabiliriz.
Gerçekten kod nesneleriyle gerçekten oynamak istiyorsanız, manuel olarak yapmak yerine ( bytecode
bittiğinde) veya byteplay
(o zamana kadar veya daha eski Python sürümleri için) gibi bir kitaplık kullanmalısınız . Bu önemsiz bir şey için bile, CodeType
başlatıcı bir acıdır; eğer gerçekten düzeltmek gibi şeyler yapmanız gerekiyorsa, bunu lnotab
sadece bir deli manuel olarak yapardı.
Ayrıca, tüm Python uygulamalarının CPython tarzı kod nesnelerini kullanmadığını söylemeye gerek yoktur. Bu kod CPython 3.7'de çalışacaktır ve muhtemelen tüm sürümler birkaç küçük değişiklikle en az 2.2'ye geri dönecektir (ve kod hackleme öğeleri değil, jeneratör ifadeleri gibi şeyler), ancak IronPython'un herhangi bir sürümü ile çalışmaz.
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()
Kod nesnelerini hacklemede ne yanlış olabilir? Çoğunlukla sadece segfaultlar, RuntimeError
tüm yığını yiyen RuntimeError
s, işlenebilen daha normal s veya muhtemelen bir TypeError
veya AttributeError
kullanmaya çalıştığınızda yükselecek çöp değerleri . Örnekler için, sadece bir kod nesnesi oluşturmayı deneyin RETURN_VALUE
(bayt yığını üzerinde hiçbir şey b'S\0'
, 3.6+ için b'S'
önce) karşılığı veya boş bir demet ile co_consts
bir olduğunda LOAD_CONST 0
byte veya birlikte varnames
1 indirildiği en yüksek böylece LOAD_FAST
aslında yükleri bir freevar / cellvar hücresi. Biraz eğlenmek için, lnotab
yeterince yanlış alırsanız , kodunuz yalnızca hata ayıklayıcıda çalıştırıldığında segfault olur.
Kullanılması bytecode
veya byteplay
tüm bu sorunların sizi korumaz, ancak bir kod yığın ekleme gibi şeyler izin bazı temel sağlık kontrolleri ve güzel yardımcıları var ve bu 'yapabilirsiniz, böylece tüm uzaklıklar ve etiketler güncellenirken üzüleyim mi yanlış anlamayın vb. (Artı, bu gülünç 6 satırlı yapıcıyı yazmaktan ve bunu yapmaktan gelen aptal yazım hatalarını ayıklamak zorunda kalmamanızı sağlıyor.)
Şimdi # 2'ye geçin.
Kod nesnelerinin değişmez olduğunu belirttim. Ve elbette ki dezavantajlar bir demettir, bu yüzden bunu doğrudan değiştiremeyiz. Ve const demetindeki şey, doğrudan değiştiremeyeceğimiz bir dizedir. Bu yüzden yeni bir kod nesnesi oluşturmak için yeni bir grup oluşturmak için yeni bir dize oluşturmak zorunda kaldım.
Peki bir dizeyi doğrudan değiştirebilseydiniz?
Kapakların altında yeterince derin, her şey sadece bazı C verilerinin bir göstergesidir, değil mi? CPython kullanıyorsanız , nesnelere erişmek için bir C API'sı vardır ve bu API'ya Python'un içinden erişmek için kullanabilirsiniz ctypes
; bu pythonapi
, stdlib'in ctypes
modülüne oraya doğru bir koymaları gibi korkunç bir fikirdir . :) Bilmeniz gereken en önemli numara , hafızadaki id(x)
asıl işaretçi olmasıdır x
(bir int
).
Maalesef, dizeler için C API, önceden dondurulmuş bir dizenin dahili depolama alanına güvenli bir şekilde girmemize izin vermiyor. Güvenli bir şekilde vidalayın, sadece başlık dosyalarını okuyalım ve bu depolamayı kendimiz bulalım.
CPython 3.4 - 3.7 kullanıyorsanız (eski sürümler için farklıdır ve geleceği bilen), saf ASCII'den yapılmış bir modülün dize hazır bilgisi, kompakt ASCII formatı kullanılarak saklanacaktır. erken biter ve ASCII bayt tamponu bellekte hemen takip eder. Dizeye ASCII olmayan bir karakter veya belirli türde değişmez olmayan dizeler koyarsanız (muhtemelen segfault'ta olduğu gibi) bu kırılır, ancak farklı dizeler için arabelleğe erişmenin diğer 4 yolunu okuyabilirsiniz.
İşleri biraz daha kolaylaştırmak için superhackyinternals
projeyi GitHub'ımdan kullanıyorum. (Yerel olarak yorumlayıcı ve benzeri yapılarınızı denemek dışında bunu kullanmamalısınız çünkü kasıtlı olarak pip-kurulmaz.)
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()
Bu şeyler ile oynamak istiyorsanız int
, kapakların altında çok daha basit str
. Ve bu değerini değiştirerek kırabilir ne olduğunu tahmin etmek çok daha kolaydır 2
için 1
sağ? Aslında, hayal etmeyi unutun, hadi yapalım (türleri superhackyinternals
tekrar kullanarak ):
>>> 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
… Kod kutusunun sonsuz uzunlukta bir kaydırma çubuğuna sahip olduğunu varsayalım.
IPython'da aynı şeyi denedim ve ilk kez 2
istemde değerlendirmeye çalıştığımda , bir çeşit kesintisiz sonsuz döngüye girdi. Muhtemelen 2
REPL döngüsünde bir şey için sayı kullanıyor , ancak hisse senedi yorumlayıcısı değil mi?
42
için23
o değerini değiştirmek için kötü bir fikir neden daha"My name is Y"
için"My name is X"
.