Установить атрибут "Только для чтения" в Python?

Учитывая динамический Python, я буду потрясен, если это не так возможно:

Я хотел бы изменить реализацию sys.stdout.write.

Я получил эту идею от ответа на другой вопрос: qaru.site/info/547740/...

Я попытался просто написать это:

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

Но он говорит мне AttributeError: 'file' object attribute 'write' is read-only.

Это хорошая попытка не дать мне сделать что-то потенциально (возможно) глупо, но я бы очень хотел пойти дальше и сделать это в любом случае. Я подозреваю, что у интерпретатора есть какая-то таблица поиска, которую я могу изменить, но я не мог найти ничего подобного в Google. __setattr__ тоже не работал - он вернул ту же самую ошибку, что и атрибут, доступный только для чтения.

Я специально ищу решение Python 2.7, если это важно, хотя нет никаких оснований сопротивляться бросанию ответов, которые работают для других версий, так как я подозреваю, что другие люди в будущем будут искать здесь похожие вопросы относительно других версий.

Ответ 1

Несмотря на свою динамичность, Python не позволяет использовать встроенные типы обезьян, такие как file. Это даже мешает вам сделать это, изменив __dict__ такого типа - свойство __dict__ возвращает dict, завернутый в прокси-сервер, доступный только для чтения, поэтому оба назначения в file.write и file.__dict__['write'] завершаются с ошибкой. И по крайней мере по двум причинам:

  • код C ожидает, что встроенный тип file соответствует структуре типа PyFile, а file.write - к функции PyFile_Write(), используемой внутренне.

  • Python реализует кэширование доступа атрибутов к типам, чтобы ускорить поиск метода и создание метода экземпляра. Этот кеш был бы сломан, если бы ему было разрешено напрямую назначать тип dicts.

Конечно же, исправление обезьян допускается для классов, реализованных в Python, которые отлично справляются с динамическими изменениями.

Однако... если вы действительно знаете, что вы делаете, вы можете использовать низкоуровневые API, такие как ctypes, чтобы подключиться к реализации и перейти к типу dict. Например:

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

Ответ 2

Несмотря на то, что Python в основном является динамическим языком, существуют такие типы объектов, как str, file (включая stdout), dict и list, которые фактически реализованы на низкоуровневом C и являются полностью статический:

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

Если ваш объект является родным кодом C, ваша единственная надежда - использовать реальный обычный класс. Для вашего случая, как уже упоминалось, вы можете сделать что-то вроде:

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

или, чтобы сделать что-то похожее на ваш исходный код:

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