22

Given how dynamic Python is, I'll be shocked if this isn't somehow possible:

I would like to change the implementation of sys.stdout.write.

I got the idea from this answer to another question of mine: https://stackoverflow.com/a/24492990/901641

I tried to simply write this:

original_stdoutWrite = sys.stdout.write

def new_stdoutWrite(*a, **kw):
    original_stdoutWrite("The new one was called! ")
    original_stdoutWrite(*a, **kw)

sys.stdout.write = new_stdoutWrite

But it tells me AttributeError: 'file' object attribute 'write' is read-only.

This is a nice attempt to keep me from doing something potentially (probably) stupid, but I'd really like to go ahead and do it anyways. I suspect the interpreter has some kind of lookup table its using that I can modify, but I couldn't find anything like that on Google. __setattr__ didn't work, either - it returned the exact same error about the attribute being read-only.

I'm specifically looking for a Python 2.7 solution, if that's important, although there's no reason to resist throwing in answers that work for other versions since I suspect other people in the future will look here with similar questions regarding other versions.

4
  • 4
    You'll have to replace all of sys.stdout with a new object that behaves like you want. Commented Jun 30, 2014 at 19:18
  • @Wooble - That's an interesting idea. Care to demonstrate in a more complete answer? Commented Jun 30, 2014 at 19:19
  • 2
    stackoverflow.com/questions/14986490/… might be helpful. Commented Jun 30, 2014 at 19:21
  • Wooble, that perfectly answered my question. Thanks! I have rolled back the title change on this question. The more specific title that was proposed strikes me as far too specific - I wanted a general answer on how to change read-only attributes (which I have been supplied with), not a specific one on how to change only the one I gave as an example in the question. Commented Jun 30, 2014 at 19:34

2 Answers 2

33

Despite its dynamicity, Python does not allow monkey-patching built-in types such as file. It even prevents you to do so by modifying the __dict__ of such a type — the __dict__ property returns the dict wrapped in a read-only proxy, so both assignment to file.write and to file.__dict__['write'] fail. And for at least two good reasons:

  1. the C code expects the file built-in type to correspond to the PyFile type structure, and file.write to the PyFile_Write() function used internally.

  2. Python implements caching of attribute access on types to speed up method lookup and instance method creation. This cache would be broken if it were allowed to directly assign to type dicts.

Monkey-patching is of course allowed for classes implemented in Python which can handle dynamic modifications just fine.

However... if you really know what you are doing, you can use the low-level APIs such as ctypes to hook into the implementation and get to the type dict. For example:

# WARNING: do NOT attempt this in production code!

import ctypes

def magic_get_dict(o):
    # find address of dict whose offset is stored in the type
    dict_addr = id(o) + type(o).__dictoffset__

    # retrieve the dict object itself
    dict_ptr = ctypes.cast(dict_addr, ctypes.POINTER(ctypes.py_object))
    return dict_ptr.contents.value

def magic_flush_mro_cache():
    ctypes.PyDLL(None).PyType_Modified(ctypes.py_object(object))

# monkey-patch file.write
dct = magic_get_dict(file)
dct['write'] = lambda f, s, orig_write=file.write: orig_write(f, '42')

# flush the method cache for the monkey-patch to take effect
magic_flush_mro_cache()

# magic!
import sys
sys.stdout.write('hello world\n')
Sign up to request clarification or add additional context in comments.

7 Comments

I think this is the type of voodoo I was looking for that should work in every case. +1 and accepted.
What's the purpose of using ctypes.cast(id(object), ctypes.py_object) instead of object?
@user2357112 PyType_Modified(object) is rejected by ctypes. The explicit cast tells ctypes what to do with the type argument - in this case, just pass it to the function as an ordinary PyObject.
@user4815162342: Looks like it does get confused by that. Personally, I'd set the function's argtypes to (py_object,), but if you want to construct a ctypes.py_object explicitly, py_object(object) would be easier than ctypes.cast. (Setting restype to None would also be a good move.)
@user2357112 Thanks, I've now amended the answer to use py_object(object) instead of the cast. Changing argtypes and restype of PyType_Modified is a bit out of scope for this answer.
|
3

Despite Python mostly being a dynamic language, there are native objects types like str, file (including stdout), dict, and list that are actually implemented in low-level C and are completely static:

>>> a = []
>>> a.append = 'something else'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'list' object attribute 'append' is read-only

>>> a.hello = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'hello'

>>> a.__dict__  # normal python classes would have this
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute '__dict__'

If your object is native C code, your only hope is to use an actual regular class. For your case, like already mentioned, you could do something like:

class NewOut(type(sys.stdout)):
    def write(self, *args, **kwargs):
        super(NewOut, self).write('The new one was called! ')
        super(NewOut, self).write(*args, **kwargs)
sys.stdout = NewOut()

or, to do something similar to your original code:

original_stdoutWrite = sys.stdout.write
class MyClass(object):
    pass
sys.stdout = MyClass()
def new_stdoutWrite(*a, **kw):
    original_stdoutWrite("The new one was called! ")
    original_stdoutWrite(*a, **kw)
sys.stdout.write = new_stdoutWrite

1 Comment

Answers the specific example I was asking about, so +1, but it doesn't seem to be a general all-purpose solution that'll work on every possible type in Python.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.