如何在Python中设置只读属性?

22

考虑到Python的动态性,如果这不可能会让我很惊讶:

我想更改sys.stdout.write的实现。

我从我的另一个问题的此答案中得到了灵感:https://dev59.com/3mAf5IYBdhLWcg3wwk0V#24492990

我尝试简单地编写了以下内容:

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'对象属性'write'是只读的

这是一个不错的尝试,以防止我做一些可能(很可能)愚蠢的事情,但我仍然想继续操作。我猜测解释器有一种查找表,可以修改,但我在Google上没有找到任何类似的东西。__setattr__也没有起作用 - 它返回完全相同的关于属性为只读的错误。

如果有必要的话,我特别寻找Python 2.7的解决方案,尽管没有理由不提供适用于其他版本的答案,因为我认为未来的其他人可能会寻找与其他版本相关的类似问题的答案。


4
你需要用一个表现你所需行为的新对象替换所有的 sys.stdout - Wooble
@Wooble - 这是一个有趣的想法。能否用更完整的答案来演示一下? - ArtOfWarfare
2
这个链接可能会有帮助:https://dev59.com/eG7Xa4cB1Zd3GeqPs7VR。 - Wooble
Wooble,你的回答非常完美地解答了我的问题。谢谢!我已经撤销了这个问题的标题更改。那个被提出的更具体的标题对我来说太具体了——我想要一个关于如何更改只读属性的通用答案(我已经得到了),而不是一个关于如何仅更改我在问题中举例的那个属性的特定答案。 - ArtOfWarfare
2个回答

33

尽管 Python 具有动态性,但它不允许对内置类型进行猴子补丁,例如 file。 它甚至通过修改此类类型的 __dict__ 来防止您这样做 —— __dict__ 属性返回包装在只读代理中的字典,因此无论是对 file.write 还是对 file.__dict__['write'] 的赋值都会失败。 这至少有两个很好的原因:

  1. C 代码期望 file 内置类型对应于 PyFile 类型结构,而 file.write 对应于内部使用的 PyFile_Write() 函数。

  2. Python 在类型上实现属性访问缓存以加速方法查找和实例方法创建。 如果直接允许对类型字典进行赋值,则此缓存将被破坏。

当然,对于在 Python 中实现的类,可以随意进行猴子补丁。

然而……如果您确实知道自己在做什么,可以使用低级 API(如 ctypes)来钩入实现并获取类型字典。例如:

# 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
我认为这就是我一直寻找的应该在每种情况下都有效的巫术类型。+1并接受。 - ArtOfWarfare
使用 ctypes.cast(id(object), ctypes.py_object) 而不是 object 的目的是什么? - user2357112
@user2357112 PyType_Modified(object)在ctypes中被拒绝。显式转换告诉ctypes如何处理类型参数 - 在这种情况下,只需将其作为普通的PyObject传递给函数即可。 - user4815162342
注意:magic_flush_mro_cache在Windows上无法使用 TypeError: LoadLibrary() argument 1 must be str, not None - movermeyer
修改了答案以实现跨平台(基于此 - movermeyer
显示剩余2条评论

3

尽管Python大多数情况下是一种动态语言,但是有一些本地对象类型,例如strfile(包括stdout),dictlist实际上是以低级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

回答了我所问的具体例子,所以+1,但它似乎不是一个通用的解决方案,不能在Python中适用于每种可能的类型。 - ArtOfWarfare

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接