Có thể hack hack chức năng in của Python Python không?


151

Lưu ý: Câu hỏi này chỉ dành cho mục đích thông tin. Tôi quan tâm để xem làm thế nào sâu vào bên trong của Python có thể đi với điều này.

Cách đây không lâu, một cuộc thảo luận đã bắt đầu bên trong một câu hỏi nhất định liên quan đến việc liệu các chuỗi được truyền cho các câu lệnh in có thể được sửa đổi sau / trong khi cuộc gọi printđược thực hiện hay không. Ví dụ, hãy xem xét chức năng:

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

Bây giờ, khi printđược chạy, thì đầu ra cho thiết bị đầu cuối sẽ hiển thị:

This dog was scared.

Lưu ý từ "mèo" đã được thay thế bằng từ "chó". Một cái gì đó ở đâu đó bằng cách nào đó có thể sửa đổi những bộ đệm nội bộ để thay đổi những gì được in. Giả sử điều này được thực hiện mà không có sự cho phép rõ ràng của tác giả mã gốc (do đó, hack / chiếm quyền điều khiển).

Nhận xét này từ @abarnert, đặc biệt, khiến tôi suy nghĩ:

Có một vài cách để làm điều đó, nhưng chúng đều rất xấu, và không bao giờ nên làm. Cách xấu nhất là có thể thay thế codeđối tượng bên trong hàm bằng một co_consts danh sách khác . Tiếp theo có lẽ là tiếp cận API C để truy cập bộ đệm bên trong của str. [...]

Vì vậy, có vẻ như điều này là thực sự có thể.

Đây là cách ngây thơ của tôi khi tiếp cận vấn đề này:

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

Tất nhiên, execlà xấu, nhưng điều đó không thực sự trả lời câu hỏi, bởi vì nó không thực sự sửa đổi bất cứ điều gì trong khi / sau khi print được gọi.

Làm thế nào nó sẽ được thực hiện như @abarnert đã giải thích nó?


3
Nhân tiện, bộ nhớ trong cho ints đơn giản hơn rất nhiều so với chuỗi và nổi hơn nữa. Và, như một phần thưởng, đó là rất nhiều rõ ràng hơn tại sao đó là một ý tưởng tồi để thay đổi giá trị của 42để 23hơn lý do tại sao đó là một ý tưởng tồi để thay đổi giá trị của "My name is Y"để "My name is X".
abarnert

Câu trả lời:


244

Đầu tiên, thực sự có một cách ít hack hơn nhiều. Tất cả những gì chúng ta muốn làm là thay đổi những gì printin, phải không?

_print = print
def print(*args, **kw):
    args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
            for arg in args)
    _print(*args, **kw)

Hoặc, tương tự, bạn có thể khỉpatch sys.stdoutthay vì print.


Ngoài ra, không có gì sai với exec … getsource …ý tưởng. Chà, dĩ ​​nhiên là có nhiều sai với nó, nhưng ít hơn những gì diễn ra ở đây


Nhưng nếu bạn muốn sửa đổi các hằng số mã của đối tượng hàm, chúng ta có thể làm điều đó.

Nếu bạn thực sự muốn chơi xung quanh với các đối tượng mã thực sự, bạn nên sử dụng một thư viện như bytecode(khi nó kết thúc) hoặc byteplay(cho đến lúc đó, hoặc cho các phiên bản Python cũ hơn) thay vì thực hiện thủ công. Ngay cả đối với một cái gì đó tầm thường này, trình CodeTypekhởi tạo là một nỗi đau; nếu bạn thực sự cần phải làm những việc như sửa chữa lnotab, chỉ có một người mất trí sẽ làm điều đó bằng tay.

Ngoài ra, không cần phải nói rằng không phải tất cả các triển khai Python đều sử dụng các đối tượng mã kiểu CPython. Mã này sẽ hoạt động trong CPython 3.7 và có thể tất cả các phiên bản trở lại ít nhất là 2.2 với một vài thay đổi nhỏ (và không phải là công cụ hack mã, nhưng những thứ như biểu thức của trình tạo), nhưng nó sẽ không hoạt động với bất kỳ phiên bản IronPython nào.

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

Điều gì có thể đi sai với hack các đối tượng mã? Chủ yếu chỉ là segfaults, RuntimeErrors ăn hết toàn bộ stack, RuntimeErrors bình thường hơn có thể được xử lý hoặc các giá trị rác có thể sẽ chỉ tăng TypeErrorhoặc AttributeErrorkhi bạn cố gắng sử dụng chúng. Ví dụ: thử tạo một đối tượng mã chỉ RETURN_VALUEkhông có gì trên ngăn xếp (mã byte b'S\0'cho 3.6+, b'S'trước đó) hoặc với một tuple trống co_constskhi có LOAD_CONST 0mã byte hoặc varnamesgiảm 1 để mức cao nhất LOAD_FASTthực sự tải một freevar / tế bào tế bào. Đối với một số niềm vui thực sự, nếu bạn nhận đượclnotab đủ sai, mã của bạn sẽ chỉ segfault khi chạy trong trình gỡ lỗi.

Sử dụng bytecodehoặc byteplaysẽ không bảo vệ bạn khỏi tất cả những vấn đề đó, nhưng chúng có một số kiểm tra vệ sinh cơ bản và những người trợ giúp tốt cho phép bạn làm những việc như chèn một đoạn mã và để nó lo lắng về việc cập nhật tất cả các lỗi và nhãn để bạn có thể ' T hiểu sai, và như vậy. (Thêm vào đó, chúng giúp bạn không phải gõ vào hàm tạo 6 dòng lố bịch đó và phải gỡ lỗi các lỗi chính tả ngớ ngẩn xuất phát từ việc này.)


Bây giờ là # 2.

Tôi đã đề cập rằng các đối tượng mã là bất biến. Và dĩ nhiên, các hằng số là một tuple, vì vậy chúng ta không thể thay đổi điều đó trực tiếp. Và thứ trong const tuple là một chuỗi, chúng ta cũng không thể thay đổi trực tiếp. Đó là lý do tại sao tôi phải xây dựng một chuỗi mới để xây dựng một bộ dữ liệu mới để xây dựng một đối tượng mã mới.

Nhưng nếu bạn có thể thay đổi một chuỗi trực tiếp thì sao?

Chà, đủ sâu dưới vỏ bọc, mọi thứ chỉ là một con trỏ đến một số dữ liệu C, phải không? Nếu bạn đang sử dụng CPython, có API C để truy cập các đối tượngbạn có thể sử dụng ctypesđể truy cập API đó từ bên trong Python, đó là một ý tưởng khủng khiếp mà họ đặt pythonapiquyền ở đó trong ctypesmô-đun của stdlib . :) Thủ thuật quan trọng nhất bạn cần biết là id(x)con trỏ thực tế xtrong bộ nhớ (dưới dạng int).

Thật không may, API C cho các chuỗi sẽ không cho phép chúng tôi nhận được một cách an toàn tại bộ lưu trữ nội bộ của một chuỗi đã bị đóng băng. Vì vậy, hãy an toàn, hãy đọc các tệp tiêu đề và tự tìm bộ lưu trữ đó.

Nếu bạn đang sử dụng CPython 3.4 - 3.7 (nó khác với các phiên bản cũ hơn và là người biết về tương lai), một chuỗi ký tự từ một mô-đun được tạo từ ASCII thuần túy sẽ được lưu trữ bằng định dạng ASCII nhỏ gọn, có nghĩa là cấu trúc kết thúc sớm và bộ đệm của byte ASCII theo ngay trong bộ nhớ. Điều này sẽ bị hỏng (như trong segfault) nếu bạn đặt một ký tự không phải ASCII trong chuỗi hoặc một số loại chuỗi không theo nghĩa đen, nhưng bạn có thể đọc 4 cách khác để truy cập bộ đệm cho các loại chuỗi khác nhau.

Để làm cho mọi thứ dễ dàng hơn một chút, tôi đang sử dụng superhackyinternalsdự án ngoài GitHub của mình. (Đó là cố ý không cài đặt được bởi vì bạn thực sự không nên sử dụng điều này ngoại trừ để thử nghiệm bản dựng trình thông dịch cục bộ của bạn và tương tự.)

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

Nếu bạn muốn chơi với những thứ này, intthì đơn giản hơn rất nhiều str. Và nó dễ dàng hơn rất nhiều để đoán những gì bạn có thể phá vỡ bằng cách thay đổi giá trị của 2để 1, phải không? Trên thực tế, hãy quên tưởng tượng, hãy làm điều đó (sử dụng các loại từ superhackyinternalsmột lần nữa):

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

Pa giả vờ rằng hộp mã có một thanh cuộn có chiều dài vô hạn.

Tôi đã thử điều tương tự trong IPython và lần đầu tiên tôi thử đánh giá 2tại dấu nhắc, nó đã đi vào một loại vòng lặp vô hạn không thể gián đoạn. Có lẽ nó đang sử dụng số 2cho một cái gì đó trong vòng REPL của nó, trong khi trình thông dịch chứng khoán thì không?


11
@ cᴏʟᴅsᴘᴇᴇᴅ Việc tạo mã được cho là Python hợp lý, mặc dù bạn thường chỉ muốn chạm vào các đối tượng mã vì nhiều lý do tốt hơn (ví dụ: chạy mã byte thông qua trình tối ưu hóa tùy chỉnh). PyUnicodeObjectMặt khác, truy cập vào bộ lưu trữ nội bộ của a , có lẽ đó thực sự chỉ là Python theo nghĩa là một trình thông dịch Python sẽ chạy nó
abarnert

4
Đoạn mã đầu tiên của bạn tăng lên NameError: name 'arg' is not defined. Ý bạn là args = [arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args]sao? Một cách tốt hơn để viết này sẽ là : args = [str(arg).replace('cat', 'dog') for arg in args]. Một tùy chọn khác, thậm chí ngắn hơn : args = map(lambda a: str(a).replace('cat', 'dog'), args). Điều này có thêm lợi ích argslà lười biếng (cũng có thể được thực hiện bằng cách thay thế sự hiểu biết danh sách ở trên bằng một trình tạo một cách *argshoạt động theo cách khác).
Konstantin

1
@ cᴏʟᴅsᴘᴇᴇᴅ Vâng, IIRC Tôi chỉ sử dụng PyUnicodeObjectđịnh nghĩa struct, nhưng sao chép nó vào câu trả lời tôi sẽ nghĩ rằng hãy hiểu và tôi nghĩ rằng các readme và / hoặc các bình luận nguồn để superhackyinternalsthực sự giải thích cách truy cập bộ đệm (ít nhất là đủ để nhắc nhở tôi lần sau tôi quan tâm, không chắc nó có đủ cho bất kỳ ai khác không), điều mà tôi không muốn vào đây. Phần có liên quan là làm thế nào để có được từ một đối tượng Python trực tiếp đến PyObject *thông qua nó ctypes. (Và có thể mô phỏng số học con trỏ, tránh char_pchuyển đổi tự động , v.v.)
abarnert

1
@ jpmc26 Tôi không nghĩ bạn cần làm điều đó trước khi nhập mô-đun, miễn là bạn làm điều đó trước khi chúng in. Các mô-đun sẽ thực hiện tra cứu tên mỗi lần, trừ khi chúng liên kết rõ ràng printvới một tên. Bạn cũng có thể liên kết tên printcho họ : import yourmodule; yourmodule.print = badprint.
leewz

1
@abarnert: Tôi nhận thấy bạn đã cảnh báo thường xuyên về việc này (ví dụ: "bạn không bao giờ muốn thực sự làm điều này" , "tại sao đó là một ý tưởng tồi để thay đổi giá trị" , v.v.). Nó không chính xác rõ ràng những gì có thể đi sai (mỉa mai), bạn sẽ sẵn sàng để giải thích một chút về điều đó? Nó có thể có thể giúp cho những người bị cám dỗ thử mù quáng.
Tôi sẽ là

37

Khỉ vá print

printlà một hàm dựng sẵn nên nó sẽ sử dụng printhàm được định nghĩa trong builtinsmô-đun (hoặc__builtin__ trong Python 2). Vì vậy, bất cứ khi nào bạn muốn sửa đổi hoặc thay đổi hành vi của hàm dựng sẵn, bạn chỉ cần gán lại tên trong mô-đun đó.

Quá trình này được gọi là 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

Sau đó, mọi printcuộc gọi sẽ đi qua custom_print, ngay cả khi cuộc gọi printnằm trong một mô-đun bên ngoài.

Tuy nhiên, bạn không thực sự muốn in văn bản bổ sung, bạn muốn thay đổi văn bản được in. Một cách để làm điều đó là thay thế nó trong chuỗi sẽ được in:

_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

Và thực sự nếu bạn chạy:

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

Hoặc nếu bạn viết nó vào một tập tin:

test_file.py

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

print_something()

và nhập nó:

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

Vì vậy, nó thực sự hoạt động như dự định.

Tuy nhiên, trong trường hợp bạn chỉ tạm thời muốn in bản vá khỉ, bạn có thể gói nó trong trình quản lý ngữ cảnh:

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

Vì vậy, khi bạn chạy nó phụ thuộc vào bối cảnh được in:

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

Vì vậy, đó là cách bạn có thể "hack" print bằng cách vá khỉ.

Sửa đổi mục tiêu thay vì print

Nếu bạn nhìn vào chữ ký của printbạn sẽ nhận thấy một fileđối số sys.stdouttheo mặc định. Lưu ý rằng đây là một đối số mặc định động (nó thực sự xuất hiện sys.stdoutmỗi khi bạn gọi print) và không giống như các đối số mặc định thông thường trong Python. Vì vậy, nếu bạn thay đổi sys.stdout printsẽ thực sự in sang mục tiêu khác thuận tiện hơn nữa, Python cũng cung cấp mộtredirect_stdout chức năng (từ Python 3.4 trở đi, nhưng thật dễ dàng để tạo một chức năng tương đương cho các phiên bản Python trước đó).

Nhược điểm là nó sẽ không hoạt động đối với các printcâu lệnh không được in sys.stdoutvà việc tạo riêng của bạn stdoutkhông thực sự đơn giản.

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

Tuy nhiên, điều này cũng hoạt động:

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

Tóm lược

Một số trong những điểm này đã được @abarnet đề cập nhưng tôi muốn khám phá các tùy chọn này chi tiết hơn. Đặc biệt là làm thế nào để sửa đổi nó trên các mô-đun (sử dụng builtins/ __builtin__) và làm thế nào để thay đổi đó chỉ là tạm thời (sử dụng ngữ cảnh).


4
Vâng, điều gần nhất với câu hỏi này mà bất kỳ ai cũng thực sự muốn làm là redirect_stdout, vì vậy thật tuyệt khi có một câu trả lời rõ ràng dẫn đến điều đó.
abarnert

6

Một cách đơn giản để nắm bắt tất cả đầu ra từ một print hàm và sau đó xử lý nó, là thay đổi luồng đầu ra thành một thứ khác, ví dụ như một tệp.

Tôi sẽ sử dụng PHPquy ước đặt tên ( 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()

Sử dụng:

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

Sẽ in

Xin chào John Bye John


5

Hãy kết hợp điều này với hướng nội khung!

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

Bạn sẽ thấy thủ thuật này mở đầu cho mọi lời chào bằng chức năng hoặc phương thức gọi. Điều này có thể rất hữu ích để đăng nhập hoặc gỡ lỗi; đặc biệt là nó cho phép bạn "đánh cắp" các câu lệnh in trong mã của bên thứ ba.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.