【问题标题】:Modifying class __dict__ when shadowed by a property被属性遮蔽时修改类 __dict__
【发布时间】:2018-07-20 22:27:42
【问题描述】:

我正在尝试使用 X.__dict__['x'] += 1 之类的东西直接修改类 __dict__ 中的值。不可能进行这样的修改,因为类__dict__ 实际上是一个不允许直接修改值的mappingproxy 对象。尝试直接修改或等效的原因是我试图将类属性隐藏在具有相同名称的元类上定义的属性后面。这是一个例子:

class Meta(type):
    def __new__(cls, name, bases, attrs, **kwargs):
        attrs['x'] = 0
        return super().__new__(cls, name, bases, attrs)
    @property
    def x(cls):
        return cls.__dict__['x']

class Class(metaclass=Meta):
    def __init__(self):
        self.id = __class__.x
        __class__.__dict__['x'] += 1

此示例显示了为 Class 的每个实例创建自动递增 ID 的方案。 __class__.__dict__['x'] += 1 行不能被 setattr(__class__, 'x', __class__.x + 1) 替换,因为 xproperty,在 Meta 中没有设置器。它只会将 TypeErrormappingproxy 更改为 AttributeErrorproperty

我试过搞乱__prepare__,但没有效果。 type 中的实现已经为命名空间返回了一个可变的dict。不可变的mappingproxy 似乎设置在type.__new__ 中,我不知道如何避免。

我还尝试将整个 __dict__ 引用重新绑定到可变版本,但同样失败:https://ideone.com/w3HqNf,这意味着 mappingproxy 可能不是在 type.__new__ 中创建的。

如何直接修改类dict 的值,即使被元类属性遮蔽?虽然它可能实际上是不可能的,但setattr 能够以某种方式做到这一点,所以我希望有一个解决方案。

我的主要要求是有一个似乎是只读的类属性,并且在任何地方都不使用其他名称。我并不完全赞同将元类 property 与同名类 dict 条目一起使用的想法,但这通常是我在常规实例中隐藏只读值的方式。

编辑

我终于弄清楚了__dict__ 类在哪里变得不可变。它在Data Model 参考的"Creating the Class Object" 部分的最后一段中进行了描述:

type.__new__ 创建一个新类时,作为命名空间参数提供的对象被复制到一个新的有序映射中,而原始对象被丢弃。新副本包装在只读代理中,成为类对象的__dict__ 属性。

【问题讨论】:

    标签: python python-3.x class


    【解决方案1】:

    可能是最好的方法:选择另一个名字。调用属性x和dict键'_x',就可以正常访问了。

    另一种方式:再增加一层间接:

    class Meta(type):
        def __new__(cls, name, bases, attrs, **kwargs):
            attrs['x'] = [0]
            return super().__new__(cls, name, bases, attrs)
        @property
        def x(cls):
            return cls.__dict__['x'][0]
    
    class Class(metaclass=Meta):
        def __init__(self):
            self.id = __class__.x
            __class__.__dict__['x'][0] += 1
    

    这样您就不必修改类字典中的实际条目。

    可能会让您的 Python 彻底发生段错误的超级黑客方式:通过 gc 模块访问底层字典。

    import gc
    
    class Meta(type):
        def __new__(cls, name, bases, attrs, **kwargs):
            attrs['x'] = 0
            return super().__new__(cls, name, bases, attrs)
        @property
        def x(cls):
            return cls.__dict__['x']
    
    class Class(metaclass=Meta):
        def __init__(self):
            self.id = __class__.x
            gc.get_referents(__class__.__dict__)[0]['x'] += 1
    

    这绕过了type.__setattr__ 为维护内部不变量所做的关键工作,尤其是在 CPython 的类型属性缓存等方面。这是一个糟糕的想法,我只是提到它,所以我可以在这里提出这个警告,因为如果其他人想出它,他们可能不知道弄乱底层的 dict 是合法的危险。

    这样做很容易导致悬空引用结束,我已经多次尝试对 Python 进行分段错误。这是crashed on Ideone的一个简单案例:

    import gc
    
    class Foo(object):
        x = []
    
    Foo().x
    gc.get_referents(Foo.__dict__)[0]['x'] = []
    
    print(Foo().x)
    

    输出:

    *** Error in `python3': double free or corruption (fasttop): 0x000055d69f59b110 ***
    ======= Backtrace: =========
    /lib/x86_64-linux-gnu/libc.so.6(+0x70bcb)[0x2b32d5977bcb]
    /lib/x86_64-linux-gnu/libc.so.6(+0x76f96)[0x2b32d597df96]
    /lib/x86_64-linux-gnu/libc.so.6(+0x7778e)[0x2b32d597e78e]
    python3(+0x2011f5)[0x55d69f02d1f5]
    python3(+0x6be7a)[0x55d69ee97e7a]
    python3(PyCFunction_Call+0xd1)[0x55d69efec761]
    python3(PyObject_Call+0x47)[0x55d69f035647]
    ... [it continues like that for a while]
    

    here's a case 的结果是错误的,并且没有嘈杂的错误消息来提醒您出现问题的事实:

    import gc
    
    class Foo(object):
        x = 'foo'
    
    print(Foo().x)
    
    gc.get_referents(Foo.__dict__)[0]['x'] = 'bar'
    
    print(Foo().x)
    

    输出:

    foo
    foo
    

    我绝对不保证任何安全的使用方式,即使事情发生在一个 Python 版本上,它们也可能不适用于未来的版本。摆弄它可能很有趣,但它不是实际使用的东西。说真的,不要这样做。您是否向您的老板解释您的网站出现故障,或者您发布的数据分析需要撤回,因为您采用了这个坏主意并使用了它?

    【讨论】:

    • 我非常喜欢这个答案。它从没有 OP 真正想听到的通常的“不要这样做”警告开始,然后逐渐演变成越来越老套的方式来实现我想要的东西。 +1,我将使用修改后的间接选项,只是使用可变整数的自定义类。
    • @MadPhysicist:我很高兴听到你至少没有选择选项 3。
    • @user2357112。我的计划是玩它,直到我完全理解它以真正使用它:)
    【解决方案2】:

    这可能算作您不想要的“附加名称”,但我已经使用元类中的字典实现了这一点,其中键是类。元类上的__next__ 方法使类本身可迭代,因此您只需执行next() 即可获得下一个ID。 dunder 方法还阻止该方法通过实例可用。存储下一个 id 的字典的名称以双下划线开头,因此从任何使用它的类都不容易发现。因此,递增 ID 功能完全包含在元类中。

    我将 id 的赋值放入基类的 __new__ 方法中,因此您不必担心 __init__ 中的它。这也可以让你del Meta,这样所有的机器都更难拿到了。

    class Meta(type):
        __ids = {}
    
        @property
        def id(cls):
            return __class__.__ids.setdefault(cls, 0)
    
        def __next__(cls):
            id = __class__.__ids.setdefault(cls, 0)
            __class__.__ids[cls] += 1
            return id
    
    class Base(metaclass=Meta):
        def __new__(cls, *args, **kwargs):
            self = object.__new__(cls)
            self.id = next(cls)
            return self
    
    del Meta
    
    class Class(Base):
        pass
    
    class Brass(Base):
        pass
    
    c0 = Class()
    c1 = Class()
    
    b0 = Brass()
    b1 = Brass()
    
    assert (b0.id, b1.id, c0.id, c1.id) == (0, 1, 0, 1)
    assert (Class.id, Brass.id) == (2, 2)
    assert not hasattr(Class, "__ids")
    assert not hasattr(Brass, "__ids")
    

    请注意,我对类和对象的属性使用了相同的名称。这样Class.id 是您创建的实例数,而c1.id 是该特定实例的 ID。

    【讨论】:

    • 我觉得使用__next__ 代替常规方法并没有真正增加太多好处。
    • 它使方法在实例中不可见,您也可以使用元类上的方法来做到这一点,但是如果要隐藏它,则很难调用它。
    • 并不是说调用 next(Class) 也会增加类 ID(可能是不可取的)
    • 如果 OP 能够修改原始代码中的 x 属性,它会做同样的事情,不是吗?但是id 类的用途却略有不同。你可以重命名它,例如count 让这一点更清楚。
    【解决方案3】:

    我的主要要求是有一个似乎是只读的类属性,并且在任何地方都不使用其他名称。我并不完全赞同使用元类 property 和同名类 dict 条目的想法,但这通常是我在常规实例中隐藏只读值的方式。

    您要求的是一个矛盾:如果您的示例有效,那么__class__.__dict__['x'] 将是该属性的“附加名称”。所以很明显,我们需要一个更具体的“附加名称”定义。但是要得出这个定义,我们需要知道您想要完成什么(注意:以下目标相互排斥,因此您可能想要做所有这些事情):

    • 您想让该值完全不可触碰,除了在Class.__init__() 方法(以及任何子类的相同方法)中:这是非Pythonic 且完全不可能的。如果__init__() 可以修改该值,那么其他任何人也可以。如果修改代码位于Class.__new__()(元类在Meta.__new__() 中动态创建)中,您可能能够完成类似的事情,但这非常难看且难以理解。
    • 您希望将操作值的代码“很好地封装”:在元类中编写一个增加私有值的方法(或执行您需要的任何其他修改),并提供一个只读元类property以公共名称访问它。
    • 您担心子类会意外地将名称与私有名称冲突:在私有名称前加上双下划线以调用 automatic name mangling。虽然这通常被认为有点不符合 Python 风格,但它适用于名称冲突对子类作者来说不太明显的情况,例如元类的内部名称与从它实例化的常规类的内部名称发生冲突。

    【讨论】:

    • 鉴于已经有两个很好的答案,他们的作者理解我的问题并提供了合法的解决方案,我认为你的有点不合适。
    • @MadPhysicist:欢迎您提出意见。但我当然希望我以后不需要阅读你的代码,如果这是你选择忽略的那种指导的话。
    猜你喜欢
    • 1970-01-01
    • 2012-07-26
    • 1970-01-01
    • 2011-06-20
    • 1970-01-01
    • 2014-09-28
    • 2016-07-24
    • 2012-09-02
    相关资源
    最近更新 更多