เป็นไปได้ที่จะ“ แฮ็ก” ฟังก์ชั่นการพิมพ์ของงูใหญ่หรือไม่


151

หมายเหตุ: คำถามนี้มีวัตถุประสงค์เพื่อให้ข้อมูลเท่านั้น ฉันสนใจที่จะดูว่าภายในของ Python นั้นมีความเป็นไปได้ที่จะทำสิ่งนี้อย่างไร

เมื่อไม่นานมานี้การสนทนาเริ่มขึ้นในคำถามที่ว่าสตริงที่ส่งผ่านไปยังคำสั่งพิมพ์สามารถแก้ไขได้หลังจาก / ระหว่างการโทรไปprintหรือไม่ ตัวอย่างเช่นพิจารณาฟังก์ชั่น:

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

ตอนนี้เมื่อprintทำงานแล้วผลลัพธ์ไปยังเทอร์มินัลควรแสดง:

This dog was scared.

สังเกตคำว่า "cat" ถูกแทนที่ด้วยคำว่า "dog" บางสิ่งบางอย่างก็สามารถปรับเปลี่ยนบัฟเฟอร์ภายในเหล่านั้นเพื่อเปลี่ยนสิ่งที่พิมพ์ สมมติว่าสิ่งนี้ทำโดยไม่ได้รับอนุญาตอย่างชัดแจ้งจากผู้เขียนรหัสดั้งเดิม (ดังนั้นการแฮ็ค / การไฮแจ็ก)

ความคิดเห็นนี้จาก @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เรียกว่า/ หลังจาก

@Barnert มีวิธีอธิบายอย่างไร?


3
อย่างไรก็ตามการจัดเก็บข้อมูลภายในสำหรับ int นั้นง่ายกว่าสตริงมากและลอยได้มากกว่านั้น และเป็นโบนัสมันมากชัดเจนมากขึ้นว่าทำไมมันเป็นความคิดที่ดีที่จะเปลี่ยนค่าของ42ไป23กว่าเหตุผลที่มันเป็นความคิดที่ดีที่จะเปลี่ยนค่าของการ"My name is Y" "My name is X"
abarnert

คำตอบ:


243

ก่อนอื่นมีวิธีแฮ็คที่น้อยกว่ามาก สิ่งที่เราต้องการทำคือเปลี่ยนสิ่งที่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)

หรือในทำนองเดียวกันคุณสามารถ monkeypatch แทนsys.stdoutprint


นอกจากนี้ไม่มีอะไรผิดปกติกับ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()

มีข้อผิดพลาดอะไรในการแฮ็กโค้ดวัตถุ ส่วนใหญ่เป็นเพียงแค่เซกค่าเริ่มต้นRuntimeErrors ที่กินสแต็กทั้งหมด, RuntimeErrors ปกติมากขึ้นที่สามารถจัดการได้หรือค่าขยะที่อาจเพิ่มTypeErrorหรือAttributeErrorเมื่อคุณพยายามที่จะใช้พวกเขา ตัวอย่างเช่นลองสร้างวัตถุรหัสที่มีเพียงRETURN_VALUEไม่มีอะไรในสแต็ก (bytecode b'S\0'สำหรับ 3.6+ b'S'ก่อน) หรือกับ tuple ที่ว่างเปล่าco_constsเมื่อมีLOAD_CONST 0ใน bytecode หรือvarnamesลดลง 1 โดยที่สูงที่สุดLOAD_FASTจริงโหลด freevar / เซลล์ cellvar เพื่อความสนุกที่แท้จริงหากคุณทำlnotabผิดพลาดรหัสของคุณจะเป็น segfault เมื่อทำงานในโปรแกรมดีบั๊กเท่านั้น

การใช้bytecodeหรือbyteplayไม่ป้องกันคุณจากปัญหาเหล่านั้นทั้งหมด แต่มีการตรวจสอบขั้นพื้นฐานบางอย่างและผู้ช่วยที่ดีที่ให้คุณทำสิ่งต่าง ๆ เช่นใส่รหัสขนาดยาวและให้กังวลเกี่ยวกับการอัปเดตและฉลากทั้งหมดเพื่อให้คุณสามารถ ไม่เข้าใจผิดและอื่น ๆ (นอกจากนี้พวกเขายังป้องกันไม่ให้คุณพิมพ์ในตัวสร้าง 6 บรรทัดที่ไร้สาระและต้องแก้จุดบกพร่องที่ผิดพลาดที่มาจากการทำเช่นนั้น)


ตอนนี้ไปที่ # 2

ฉันพูดถึงว่ารหัสวัตถุไม่เปลี่ยนรูป และแน่นอนว่า consts นั้นเป็นสิ่งอันดับดังนั้นเราจึงไม่สามารถเปลี่ยนแปลงได้โดยตรง และสิ่งที่อยู่ใน const tuple คือสตริงซึ่งเราก็ไม่สามารถเปลี่ยนแปลงได้โดยตรง นั่นเป็นเหตุผลที่ฉันต้องสร้างสตริงใหม่เพื่อสร้าง tuple ใหม่เพื่อสร้างวัตถุรหัสใหม่

แต่ถ้าคุณสามารถเปลี่ยนสตริงได้โดยตรง

ลึกพอที่จะครอบคลุมทุกอย่างเป็นเพียงตัวชี้ไปยังข้อมูล C ใช่ไหม? หากคุณกำลังใช้ CPython มีซี API เพื่อการเข้าถึงวัตถุและคุณสามารถใช้ctypesเพื่อเข้าถึง API จากภายในหลามตัวเองซึ่งเป็นเช่นความคิดที่น่ากลัวว่าพวกเขาใส่pythonapiมีสิทธิใน STDLIB ของctypesโมดูล :) เคล็ดลับที่สำคัญที่สุดที่คุณต้องรู้ก็คือนั่นid(x)คือตัวชี้จริงxในหน่วยความจำ (ตามint)

น่าเสียดายที่ C API สำหรับสตริงจะไม่อนุญาตให้เราไปถึงที่เก็บข้อมูลภายในของสตริงที่ถูกตรึงแล้วอย่างปลอดภัย ดังนั้นขันอย่างปลอดภัยเรามาอ่านไฟล์ส่วนหัวและค้นหาที่เก็บข้อมูลเอง

หากคุณใช้ CPython 3.4 - 3.7 (แตกต่างจากรุ่นเก่าและผู้ที่รู้อนาคต) สตริงตัวอักษรจากโมดูลที่ทำจาก ASCII บริสุทธิ์จะถูกจัดเก็บโดยใช้รูปแบบ ASCII ขนาดกะทัดรัดซึ่งหมายถึงโครงสร้าง สิ้นสุดลง แต่เนิ่น ๆ และบัฟเฟอร์ของ ASCII ไบต์จะตามมาในหน่วยความจำทันที สิ่งนี้จะทำให้แตก (เช่นใน segfault) หากคุณใส่อักขระที่ไม่ใช่ ASCII ในสตริงหรือสตริงที่ไม่ใช่ตัวอักษรบางชนิด แต่คุณสามารถอ่านวิธี 4 วิธีอื่นในการเข้าถึงบัฟเฟอร์สำหรับสตริงประเภทต่างๆ

เพื่อให้สิ่งต่าง ๆ ง่ายขึ้นเล็กน้อยฉันใช้superhackyinternalsโครงการนี้เพื่อปิด GitHub ของฉัน (ไม่สามารถติดตั้ง 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ᴘᴇᴇᴅรหัส-munging เป็นเนื้อหาที่เหมาะสมหลามแม้ว่าโดยทั่วไปคุณเพียงต้องการที่จะสัมผัสวัตถุสำหรับเหตุผลที่ดีมาก (เช่นวิ่ง bytecode ผ่านการเพิ่มประสิทธิภาพที่กำหนดเอง) ในทางกลับกันการเข้าถึงที่เก็บข้อมูลภายในของ a PyUnicodeObject, นั่นอาจเป็นเพียง Python เท่านั้นในแง่ที่ล่าม Python จะเรียกใช้ ...
abarnert

4
รหัสแรกของคุณ snippet 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ทำงานได้ทั้งสองวิธี)
Konstantin

1
@ cᴏʟᴅsᴘᴇᴇᴅใช่ IIRC ฉันแค่ใช้PyUnicodeObjectคำจำกัดความของโครงสร้าง แต่คัดลอกที่เป็นคำตอบที่ฉันคิดว่าจะได้รับในทางและฉันคิดว่า readme และ / หรือความคิดเห็นแหล่งที่มาเพื่อsuperhackyinternalsอธิบายวิธีการเข้าถึงบัฟเฟอร์จริง ๆ (อย่างน้อย ดีพอที่จะเตือนฉันในครั้งต่อไปที่ฉันแคร์ไม่แน่ใจว่ามันจะเพียงพอสำหรับคนอื่น ... ) ซึ่งฉันไม่ต้องการเข้ามาที่นี่ ส่วนที่เกี่ยวข้องเป็นวิธีการที่จะได้รับจากวัตถุหลามสดให้มันผ่านPyObject * ctypes(และอาจจำลองเลขคณิตตัวชี้การหลีกเลี่ยงchar_pการแปลงอัตโนมัติฯลฯ )
abarnert

1
@ jpmc26 ฉันไม่คิดว่าคุณต้องทำก่อนที่จะนำเข้าโมดูลตราบใดที่คุณทำก่อนที่พวกเขาจะพิมพ์ โมดูลจะทำการค้นหาชื่อทุกครั้งยกเว้นว่าจะผูกprintกับชื่ออย่างชัดเจน นอกจากนี้คุณยังสามารถผูกชื่อสำหรับพวกเขา:print import yourmodule; yourmodule.print = badprint
leewz

1
@abarnert: ฉันสังเกตเห็นว่าคุณได้รับการเตือนบ่อยครั้งเกี่ยวกับการทำเช่นนี้ (เช่น"คุณไม่เคยต้องการทำสิ่งนี้จริง ๆ " , "ทำไมจึงเป็นความคิดที่ดีที่จะเปลี่ยนค่า"ฯลฯ ) ไม่ชัดเจนว่าจะมีอะไรผิดพลาด (การเสียดสี) คุณยินดีที่จะอธิบายรายละเอียดเล็กน้อยในเรื่องนั้นหรือไม่? มันอาจช่วยให้ผู้ที่ถูกลองไม่ได้ลอง
l'L'l

37

ลิงแพทช์ print

printเป็นฟังก์ชันในตัวดังนั้นจึงจะใช้printฟังก์ชันที่กำหนดในbuiltinsโมดูล (หรือ__builtin__ใน Python 2) ดังนั้นเมื่อใดก็ตามที่คุณต้องการแก้ไขหรือเปลี่ยนแปลงพฤติกรรมของฟังก์ชัน builtin คุณสามารถกำหนดชื่อในโมดูลนั้นใหม่ได้

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_printแม้ว่าprintจะอยู่ในโมดูลภายนอก

อย่างไรก็ตามคุณไม่ต้องการพิมพ์ข้อความเพิ่มเติมคุณต้องการเปลี่ยนข้อความที่พิมพ์ วิธีหนึ่งในการดำเนินการดังกล่าวคือการแทนที่ในสตริงที่จะพิมพ์:

_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.

ดังนั้นจึงได้ผลตามที่ต้องการ

อย่างไรก็ตามในกรณีที่คุณต้องการพิมพ์ตัวแก้ไขลิงชั่วคราวคุณสามารถใช้สิ่งนี้ใน context-manager:

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คุณจะสังเกตเห็นfileข้อโต้แย้งที่เป็นsys.stdoutค่าเริ่มต้น โปรดทราบว่านี่เป็นอาร์กิวเมนต์เริ่มต้นแบบไดนามิก ( จริงๆแล้วมันค้นหาsys.stdoutทุกครั้งที่คุณโทรprint) และไม่ชอบข้อโต้แย้งเริ่มต้นปกติในงูหลาม ดังนั้นหากคุณเปลี่ยนไปsys.stdout printจะพิมพ์ไปยังเป้าหมายที่แตกต่างกันซึ่งสะดวกยิ่งกว่าที่ Python จะให้redirect_stdoutฟังก์ชั่น (จาก Python 3.4 บน แต่มันง่ายที่จะสร้างฟังก์ชั่นที่เทียบเท่าสำหรับ Python เวอร์ชันก่อนหน้า)

ข้อเสียคือมันจะไม่ทำงานสำหรับ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

6

วิธีง่ายๆในการจับเอาท์พุททั้งหมดจากprintฟังก์ชั่นแล้วประมวลผลคือการเปลี่ยนสตรีมเอาต์พุตเป็นอย่างอื่นเช่นไฟล์

ฉันจะใช้แบบPHPแผนการตั้งชื่อ ( 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"))

จะพิมพ์

สวัสดี John Bye John


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.