क्या पायथन के प्रिंट फ़ंक्शन को "हैक" करना संभव है?


151

नोट: यह प्रश्न केवल सूचना के उद्देश्यों के लिए है। मुझे यह देखने में दिलचस्पी है कि पायथन के इंटर्नल में कितना गहरा है, इसके साथ जाना संभव है।

बहुत पहले नहीं, एक निश्चित प्रश्न के अंदर एक चर्चा शुरू हुई कि क्या स्टेटमेंट को प्रिंट करने के लिए पारित printकिया गया था जिसे कॉल करने के दौरान / उसके दौरान संशोधित किया जा सकता है। उदाहरण के लिए, फ़ंक्शन पर विचार करें:

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

अब, जब printचलाया जाता है, तब टर्मिनल को आउटपुट प्रदर्शित करना चाहिए:

This dog was scared.

"बिल्ली" शब्द को "कुत्ते" शब्द से बदल दिया गया है। कहीं न कहीं कुछ तो उन आंतरिक बफ़र्स को संशोधित करने में सक्षम था जो बदल गया था। मान लें कि यह मूल कोड लेखक की स्पष्ट अनुमति के बिना किया गया है (इसलिए, हैकिंग / अपहरण)।

विशेष रूप से, बुद्धिमान @abarnert की यह टिप्पणी मुझे मिली:

ऐसा करने के कुछ तरीके हैं, लेकिन वे सभी बहुत बदसूरत हैं, और कभी नहीं किया जाना चाहिए। कम से कम बदसूरत तरीका संभवतः codeएक अलग co_consts सूची के साथ फ़ंक्शन के अंदर ऑब्जेक्ट को प्रतिस्थापित करना है । अगला शायद स्ट्रिंग के आंतरिक बफर तक पहुंचने के लिए सी एपीआई में पहुंच रहा है। [...]

तो, ऐसा लगता है कि यह वास्तव में संभव है।

यहाँ इस समस्या से निपटने का मेरा भोला तरीका है:

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

बेशक, execबुरा है, लेकिन यह वास्तव में सवाल का जवाब नहीं देता है, क्योंकि यह वास्तव में कुछ भी संशोधित नहीं करता है जब / बाद print में कहा जाता है।

यह कैसे किया जाएगा क्योंकि @abarnert ने इसे समझाया है?


3
वैसे, इन्ट्स के लिए आंतरिक भंडारण स्ट्रिंग्स की तुलना में बहुत सरल है, और इससे भी अधिक तैरता है। और, एक बोनस के रूप में, यह एक बहुत अधिक स्पष्ट है क्यों यह एक बुरा विचार के मान बदलने के लिए है 42करने के लिए 23कारण है कि यह एक बुरा विचार के मान बदलने के लिए है की तुलना में "My name is Y"करने के लिए "My name is X"
abarnert

जवाबों:


243

सबसे पहले, वहाँ वास्तव में एक बहुत कम hacky तरीका है। हम सब करना चाहते हैं क्या 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)

या, इसी तरह, आप के sys.stdoutबजाय बंदरों को पकड़ सकते हैं print


इसके अलावा, exec … getsource …विचार के साथ कुछ भी गलत नहीं है । ठीक है, निश्चित रूप से इसके साथ बहुत गलत है, लेकिन यहाँ क्या है की तुलना में कम ...


लेकिन यदि आप फ़ंक्शन ऑब्जेक्ट के कोड कॉन्स्टेंट को संशोधित करना चाहते हैं, तो हम ऐसा कर सकते हैं।

यदि आप वास्तव में असली के लिए कोड ऑब्जेक्ट्स के साथ खेलना चाहते हैं, तो आपको मैन्युअल रूप से करने के बजाय एक लाइब्रेरी का उपयोग करना चाहिए bytecode( जैसे कि यह समाप्त हो गया है) या byteplay(तब तक, या पुराने पायथन संस्करणों के लिए)। यहां तक ​​कि कुछ के लिए यह तुच्छ, CodeTypeइनिशियलाइज़र एक दर्द है; यदि आपको वास्तव में सामान को ठीक करने की आवश्यकता है lnotab, तो केवल एक चंचल व्यक्ति स्वयं ऐसा करेगा।

इसके अलावा, यह कहे बिना जाता है कि सभी पायथन कार्यान्वयन CPython- शैली कोड ऑब्जेक्ट का उपयोग नहीं करते हैं। यह कोड CPython 3.7 में काम करेगा, और शायद सभी संस्करण कुछ मामूली बदलावों के साथ कम से कम 2.2 में वापस आ जाएंगे (और कोड-हैकिंग सामान नहीं, लेकिन जनरेटर के भाव जैसी चीजें), लेकिन यह आयरनपिथॉन के किसी भी संस्करण के साथ काम नहीं करेगा।

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

कोड वस्तुओं को हैक करने में क्या गलत हो सकता है? ज्यादातर सिर्फ segfaults, RuntimeErrors जो पूरे स्टैक को खाते हैं, अधिक सामान्य RuntimeErrors जो संभाला जा सकता है, या कचरा मान जो शायद सिर्फ एक को उठाएगा TypeErrorया AttributeErrorजब आप उनका उपयोग करने का प्रयास करेंगे। उदाहरण के लिए, RETURN_VALUEस्टैक पर कुछ भी नहीं के साथ एक कोड ऑब्जेक्ट बनाने का प्रयास करें (बायटेकोड b'S\0'3.6+ के लिए, b'S'इससे पहले), या co_constsजब बायटेकोड LOAD_CONST 0में है, या varnames1 से घटाया गया है, तो सबसे अधिक LOAD_FASTवास्तव में एक फ्रीवर लोड करता है के लिए एक खाली ट्यूपल के साथ / सेलवार सेल। कुछ वास्तविक मौज-मस्ती के लिए, यदि आप lnotabगलत गलत करते हैं, तो आपका कोड केवल डिबगर में चलने पर सीगफॉल्ट होगा।

का उपयोग करना bytecodeया byteplayउन समस्याओं के सभी से बचाने नहीं होगा, लेकिन वे कुछ बुनियादी विवेक चेक, और अच्छा सहायकों कि आप कोड का एक हिस्सा डालने जैसे कार्य करने देते हैं और यह तो आप कर सकते हैं 'सभी ऑफसेट और लेबल अपडेट के बारे में चिंता करते हैं टी यह गलत है, और इतने पर। (साथ ही, वे आपको उस हास्यास्पद 6-लाइन कंस्ट्रक्टर में टाइप करने के लिए रखते हैं, और ऐसा करने से आने वाले मूर्ख टाइपो को डीबग करने के लिए रखते हैं।)


अब # 2 पर।

मैंने उल्लेख किया है कि कोड ऑब्जेक्ट अपरिवर्तनीय हैं। और निश्चित रूप से कब्ज टपल हैं, इसलिए हम इसे सीधे नहीं बदल सकते हैं। और const tuple में चीज़ एक स्ट्रिंग है, जिसे हम सीधे बदल भी नहीं सकते हैं। इसलिए मुझे नए कोड ऑब्जेक्ट बनाने के लिए एक नया टपल बनाने के लिए एक नया स्ट्रिंग बनाना था।

लेकिन क्या होगा अगर आप सीधे एक स्ट्रिंग बदल सकते हैं?

अच्छी तरह से, कवर के नीचे पर्याप्त गहरा, सब कुछ बस कुछ सी डेटा के लिए एक संकेतक है, है ना? यदि आप CPython का उपयोग कर रहे हैं, तो वस्तुओं तक पहुँचने के लिए एक C API है , और आप ctypesPython के भीतर से ही उस API तक पहुँचने के लिए उपयोग कर सकते हैं , जो इतना भयानक विचार है कि वे pythonapiवहाँ stdlib के ctypesमॉड्यूल में एक सही जगह डालते हैं । :) सबसे महत्वपूर्ण चाल जिसे आपको जानना आवश्यक है वह id(x)यह है कि xमेमोरी में वास्तविक सूचक है (एक int)।

दुर्भाग्य से, तार के लिए सी एपीआई हमें पहले से जमे हुए स्ट्रिंग के आंतरिक भंडारण में सुरक्षित रूप से नहीं आने देगा। तो सुरक्षित रूप से पेंच, चलो हेडर फ़ाइलों को पढ़ते हैं और उस भंडारण को स्वयं ढूंढते हैं।

यदि आप CPython 3.4 - 3.7 का उपयोग कर रहे हैं (यह पुराने संस्करणों के लिए अलग है, और जो भविष्य के लिए जानता है), एक मॉड्यूल से एक स्ट्रिंग शाब्दिक जो शुद्ध ASCII से बना है, कॉम्पैक्ट ASCII प्रारूप का उपयोग करके संग्रहीत किया जा रहा है, जिसका अर्थ है कि संरचना जल्दी समाप्त हो जाता है और ASCII बाइट्स का बफर स्मृति में तुरंत बाद में आता है। यह टूट जाएगा (जैसा कि शायद सेगफॉल्ट में) यदि आप स्ट्रिंग में एक गैर-एएससीआईआई चरित्र, या कुछ प्रकार के गैर-शाब्दिक तार लगाते हैं, लेकिन आप विभिन्न प्रकार के तारों के लिए बफर तक पहुंचने के अन्य 4 तरीकों पर पढ़ सकते हैं।

चीजों को थोड़ा आसान बनाने के लिए, मैं superhackyinternalsअपने GitHub से परियोजना का उपयोग कर रहा हूं । (यह जानबूझकर पाइप-इंस्टॉल करने योग्य नहीं है क्योंकि आप वास्तव में इंटरप्रेटर और अपने स्थानीय बिल्ड के साथ प्रयोग करने के अलावा इसका उपयोग नहीं करना चाहिए।)

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

... बहाना है कि कोड बॉक्स में एक अनंत-लंबाई स्क्रॉलबार है।

मैंने आईपीथॉन में एक ही चीज़ की कोशिश की, और पहली बार जब मैंने 2प्रॉम्प्ट पर मूल्यांकन करने की कोशिश की , तो यह किसी प्रकार के अबाधित अनंत लूप में चला गया। संभवतः यह 2अपने REPL लूप में कुछ के लिए संख्या का उपयोग कर रहा है , जबकि स्टॉक दुभाषिया नहीं है?


11
@ c @sably कोड-मुंगिंग यकीनन उचित पायथन है, हालाँकि आप आमतौर पर केवल कोड ऑब्जेक्ट्स को बहुत बेहतर कारणों से छूना चाहते हैं (उदाहरण के लिए, कस्टम ऑप्टिमाइज़र के माध्यम से बाईटकोड चलाना)। PyUnicodeObjectदूसरी ओर, के आंतरिक भंडारण तक पहुँचना , यह वास्तव में केवल पायथन है इस अर्थ में कि पायथन दुभाषिया इसे चलाएगा ...
7:14

4
आपका पहला कोड स्निपेट उठता है 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दोनों तरह से काम करता है)।
कॉन्सटेंटिन

1
@ c @s the हाँ, IIRC मैं केवल PyUnicodeObjectसंरचना की परिभाषा का उपयोग कर रहा हूं , लेकिन इसका उत्तर देते हुए कि मुझे लगता है कि मैं बस रास्ते में मिल जाऊंगा , और मुझे लगता है कि रीडमी और / या स्रोत टिप्पणियां superhackyinternalsवास्तव में बताती हैं कि बफर का उपयोग कैसे करें (कम से कम अगली बार मुझे याद दिलाने के लिए पर्याप्त है; सुनिश्चित नहीं है कि यह किसी और के लिए पर्याप्त होगा ...), जो मैं यहां नहीं जाना चाहता था। प्रासंगिक हिस्सा यह है कि लाइव पायथन ऑब्जेक्ट को इसके PyObject *माध्यम से कैसे प्राप्त किया जाए ctypes। (और शायद पॉइंटर अंकगणित का अनुकरण करना, स्वचालित char_pरूपांतरणों से बचना , आदि)
एबार्नर्ट

1
@ jpmc26 मुझे नहीं लगता कि आपको मॉड्यूल आयात करने से पहले ऐसा करने की आवश्यकता है , जब तक आप इसे प्रिंट करने से पहले करते हैं। मॉड्यूल हर बार नाम देखने का काम करेंगे, जब तक कि वे स्पष्ट रूप printसे एक नाम से न बंधें । आप printउनके लिए नाम भी बाँध सकते हैं import yourmodule; yourmodule.print = badprint:।
लेवेज

1
@abarnert: मैंने देखा है कि आपने अक्सर ऐसा करने के बारे में चेतावनी दी है (उदाहरण के लिए। आप वास्तव में ऐसा कभी नहीं करना चाहते हैं " , " क्यों यह मूल्य बदलने के लिए एक बुरा विचार है " , आदि)। यह बिल्कुल स्पष्ट नहीं है कि संभवतः क्या गलत हो सकता है (व्यंग्य), क्या आप उस पर थोड़ा विस्तार करने के लिए तैयार होंगे? यह संभवतः उन लोगों के लिए मदद कर सकता है जो इसे आँख बंद करके आज़माते हैं।
l'L'l

37

बंदर-पैच print

printएक बिलिन फ़ंक्शन है इसलिए यह मॉड्यूल (या पायथन 2) में printपरिभाषित फ़ंक्शन का उपयोग करेगा । इसलिए जब भी आप एक अंतर्निहित फ़ंक्शन के व्यवहार को संशोधित या बदलना चाहते हैं तो आप बस उस मॉड्यूल में नाम को पुन: असाइन कर सकते हैं।builtins__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.

तो यह वास्तव में इरादा के रूप में काम करता है।

हालाँकि, यदि आप केवल अस्थायी रूप से बंदर-पैच प्रिंट चाहते हैं तो आप इसे एक संदर्भ-प्रबंधक में लपेट सकते हैं:

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वास्तव में अलग-अलग लक्ष्य के लिए और भी अधिक सुविधाजनक होगा कि पायथन भी एक redirect_stdoutफ़ंक्शन प्रदान करता है (पायथन 3.4 पर से), लेकिन पहले पायथन संस्करणों के लिए एक समान फ़ंक्शन बनाना आसान है)।

नकारात्मक पक्ष यह है कि यह उन 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__) और उस परिवर्तन को केवल अस्थायी (संदर्भकर्ता का उपयोग करके) कैसे किया जाए।


4
हाँ, इस सवाल का सबसे करीबी व्यक्ति जो कभी भी वास्तव में करना चाहता है redirect_stdout, इसलिए यह स्पष्ट है कि इसका उत्तर स्पष्ट है।
गाली

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

छपता होगा

हाय जॉन बाय जॉन


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

आपको यह ट्रिक कॉलिंग फंक्शन या मेथड के साथ हर अभिवादन के लिए मिल जाएगी। लॉगिंग या डीबगिंग के लिए यह बहुत उपयोगी हो सकता है; विशेष रूप से यह आपको तीसरे पक्ष के कोड में "अपहृत" प्रिंट स्टेटमेंट की सुविधा देता है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.