Python'un yazdırma işlevini “hacklemek” mümkün mü?


151

Not: Bu soru yalnızca bilgi amaçlıdır. Python'un içlerine ne kadar derine inmenin mümkün olduğunu görmekle ilgileniyorum.

Çok uzun zaman önce, belirli bir soru içinde , basılı çağrılara geçirilen dizelerin çağrı yapıldıktan sonra / sırasında değiştirilip değiştirilemeyeceğine dair bir tartışma başladı print. Örneğin, şu işlevi göz önünde bulundurun:

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

Şimdi, printçalıştırıldığında, terminale çıkış şunları göstermelidir:

This dog was scared.

"Kedi" kelimesinin yerini "köpek" kelimesi aldı. Bir yerlerde bir şey, yazdırılanları değiştirmek için bu dahili tamponları değiştirebildi. Bunun orijinal kod yazarının açık izni olmadan yapıldığını varsayın (dolayısıyla, hackleme / kaçırma).

Bilge @abarnert'ın bu yorumu özellikle beni düşündürdü:

Bunu yapmanın birkaç yolu var, ama hepsi çok çirkin ve asla yapılmamalıdır. En çirkin yol muhtemelen codefonksiyonun içindeki nesneyi farklı bir co_consts listeyle değiştirmektir. Sonraki, str'nin ara belleğine erişmek için muhtemelen C API'sine ulaşıyor. [...]

Yani, aslında bu mümkün görünüyor.

İşte bu soruna yaklaşmanın saf yolu:

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

Tabii ki, execkötü, ama bu soruya gerçekten cevap vermiyor, çünkü çağrıldığında / sonrasında aslında hiçbir şeyi değiştirmiyor print.

@Abarnert'ın açıkladığı gibi bu nasıl olurdu?


3
Bu arada, ints için dahili depolama dizelerden çok daha basittir ve daha da yüzer. O değerini değiştirmek için kötü bir fikirdir Ve neden, bir bonus olarak, çok daha fazla açıktır 42için 23o değerini değiştirmek için kötü bir fikir neden daha "My name is Y"için "My name is X".
abarnert

Yanıtlar:


243

İlk olarak, aslında çok daha az hileli bir yol var. Tüm yapmak istediğimiz printbaskı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.stdoutbunun 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 ( bytecodebittiğ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, CodeTypebaşlatıcı bir acıdır; eğer gerçekten düzeltmek gibi şeyler yapmanız gerekiyorsa, bunu lnotabsadece 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, RuntimeErrortüm yığını yiyen RuntimeErrors, işlenebilen daha normal s veya muhtemelen bir TypeErrorveya AttributeErrorkullanmaya ç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_constsbir olduğunda LOAD_CONST 0byte veya birlikte varnames1 indirildiği en yüksek böylece LOAD_FASTaslında yükleri bir freevar / cellvar hücresi. Biraz eğlenmek için, lnotabyeterince yanlış alırsanız , kodunuz yalnızca hata ayıklayıcıda çalıştırıldığında segfault olur.

Kullanılması bytecodeveya byteplaytü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 ctypesmodü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 superhackyinternalsprojeyi 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 2için 1sağ? Aslında, hayal etmeyi unutun, hadi yapalım (türleri superhackyinternalstekrar 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 2istemde değerlendirmeye çalıştığımda , bir çeşit kesintisiz sonsuz döngüye girdi. Muhtemelen 2REPL döngüsünde bir şey için sayı kullanıyor , ancak hisse senedi yorumlayıcısı değil mi?


11
@ cᴏʟᴅsᴘᴇᴇᴅ Kod-munging tartışmalı olarak makul Python'dur, ancak genellikle kod nesnelerine çok daha iyi nedenlerle dokunmak istersiniz (örn., bayt kodunu özel bir iyileştirici aracılığıyla çalıştırmak). PyUnicodeObjectÖte yandan,
a'nın

4
İlk kod snippet'iniz yükselir NameError: name 'arg' is not defined. Şunu mu demek istedin args = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]? Bu yazma bir tartışmalı iyi bir yol olacaktır: args = [str(arg).replace('cat', 'dog') for arg in args]. Başka, daha kısa, opsiyon: args = map(lambda a: str(a).replace('cat', 'dog'), args). Bu argstembel ek bir yararı vardır (bu da yukarıdaki liste kavrayışı bir jeneratör ile değiştirilerek başarılabilir - *argsher iki şekilde de çalışır).
Konstantin

1
@ cᴏʟᴅsᴘᴇᴇᴅ Evet, IIRC Sadece PyUnicodeObjectstruct tanımını kullanıyorum , ancak cevaba kopyalamak sadece yol almayı düşünürdüm ve bence ben ve / veya kaynak yorumları superhackyinternalsaslında ara belleğe nasıl erişileceğini açıklamak için (en azından bir dahaki sefere umursadığımı hatırlatmak için yeterince iyi; buraya girmek istemediğim başka biri için yeterli olup olmadığından emin değilim. İlgili kısmı onun için canlı Python nesneden nasıl olduğunu PyObject *üzeri ctypes. (Ve belki işaretçi aritmetiğini simüle eder, otomatik char_pdönüşümlerden kaçınır , vb.)
abarnert

1
@ jpmc26 Bunu yapmak gerekir sanmıyorum önce yeter ki yazdırmak önce bunu gibi ithal modüllerin. Modüller, printbir isme açıkça bağlanmadığı sürece her seferinde isim araması yapar . Ayrıca adı bağlayabilir printonlar için: import yourmodule; yourmodule.print = badprint.
leewz

1
@abarnert: Bunu yaparken sık sık uyardığınızı fark ettim (örn. "bunu asla yapmak istemezsiniz" , "neden değeri değiştirmek kötü bir fikir" vb.). Neyin yanlış gidebileceği tam olarak belli değil (alaycılık), bununla ilgili biraz ayrıntıya girmeye istekli misiniz? Belki körü körüne denemek için cazip olanlar için yardımcı olabilir.
l'L'l

37

Maymun-yama print

printyerleşik bir işlevdir, dolayısıyla modülde (veya Python 2'de) printtanımlanan işlevi kullanır . Böylece, yerleşik bir işlevin davranışını değiştirmek veya değiştirmek istediğinizde, o modüldeki adı yeniden atayabilirsiniz.builtins__builtin__

Bu işleme denir 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

Bundan sonra, harici bir modülde olsa bile , her printçağrı gerçekleşir .custom_printprint

Ancak gerçekten ek metin yazdırmak istemezsiniz, yazdırılan metni değiştirmek istersiniz. Bunu yapmanın bir yolu, yazdırılacak dizede değiştirmektir:

_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

Ve gerçekten koşarsanız:

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

Veya bunu bir dosyaya yazarsanız:

test_file.py

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

print_something()

ve içe aktarın:

>>> import test_file
This dog was scared.
>>> test_file.print_something()
This dog was scared.

Yani gerçekten amaçlandığı gibi çalışıyor.

Bununla birlikte, yalnızca geçici olarak maymun yaması yazdırmak istiyorsanız, bunu bir bağlam yöneticisine sarabilirsiniz:

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

Bu nedenle, çalıştırdığınızda, yazdırılan içeriğe bağlıdır:

>>> with ChangePrint() as x:
...     test_file.print_something()
... 
This dog was scared.
>>> test_file.print_something()
This cat was scared.

Böylece printmaymun yama ile "hack" yapabilirsiniz .

Hedefi yerine hedefi değiştirin print

İmzasına bakarsanız, varsayılan olarak printbir fileargüman görürsünüz sys.stdout. Bunun dinamik bir varsayılan bağımsız değişken olduğunu ( her aradığınızda gerçekten bakar ) ve Python'daki normal varsayılan bağımsız değişkenler gibi olmadığını unutmayın. Yani, eğer değiştirirseniz aslında Python'un bir işlev sağlaması daha farklı bir hedefe yazdırılacaktır (Python 3.4'ten itibaren, ancak önceki Python sürümleri için eşdeğer bir işlev oluşturmak kolaydır).sys.stdoutprintsys.stdout printredirect_stdout

Dezavantajı, printyazdırılmayan ifadeler için işe yaramayacağı sys.stdoutve kendinizinkini oluşturmanın stdoutgerçekten basit olmadığıdır.

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

Ancak bu da işe yarar:

>>> import contextlib
>>> with contextlib.redirect_stdout(CustomStdout()):
...     test_file.print_something()
... 
This dog was scared.
>>> test_file.print_something()
This cat was scared.

özet

Bu noktalardan bazıları @abarnet tarafından zaten belirtilmiştir, ancak bu seçenekleri daha ayrıntılı olarak incelemek istedim. Özellikle modüller arasında nasıl değiştirileceği ( builtins/ __builtin__) ve bu değişikliğin nasıl geçici hale getirileceği (bağlam yöneticileri kullanılarak).


4
Evet, bu soruya herkesin gerçekten yapmak istediği en yakın şey şudur redirect_stdout, bu yüzden buna yol açan açık bir cevaba sahip olmak güzel.
abarnert

6

Bir printişlevden tüm çıktıları yakalamanın ve ardından işlemenin basit bir yolu , çıktı akışını başka bir şeye, örneğin bir dosyaya değiştirmektir.

Bir PHPadlandırma kuralı kullanacağım ( 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()

Kullanımı:

print ("Hi John")
ob_start()
print ("Hi John")
ob_end()
print (ob_get_contents().replace("Hi", "Bye"))

Yazdırır

Merhaba John Bye John


5

Bunu çerçeve içgözlemiyle birleştirelim!

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

Bu numarayı arama fonksiyonu veya yöntemiyle her selamlama öncesinde bulabilirsiniz. Bu, günlüğe kaydetme veya hata ayıklama için çok yararlı olabilir; özellikle üçüncü taraf kodunda baskıları "kaçırmak" sağlar gibi.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.