在被属性遮蔽时修改类__dict__

时间:2018-02-10 06:20:55

标签: python python-3.x class

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

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),因为xpropertyMeta中没有设置者。它只会将TypeErrormappingproxy更改为AttributeError中的property

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

我还试图将整个__dict__引用重新绑定到一个可变版本,但也失败了:https://ideone.com/w3HqNf,暗示mappingproxy可能未在{{1}中创建}}

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

我的主要要求是拥有一个看似只读的类属性,并且不会在任何地方使用其他名称。我并非完全不习惯使用元类setattr和同名类property条目,但这通常是我在常规实例中隐藏只读值的方式。

修改

我终于找到了类dict变为不可变的位置。 "Creating the Class Object"引用的Data Model部分的最后一段中描述了它:

  

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

3 个答案:

答案 0 :(得分:6)

可能是最好的方法:选择另一个名字。调用属性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

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

超级hacky方式可能会彻底破坏你的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的类型属性缓存这样的东西。这是一个可怕的想法,我只是提到它,所以我可以在这里发出这个警告,因为如果其他人提出这个警告,他们可能不会知道搞乱根本的咒语是合法的危险。

这样做很容易让悬空引用结束,而且我已经尝试过几次试验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版本上运行,它们也可能无法在未来版本上运行。摆弄它可能很有趣,但它不是实际使用的东西。说真的,不要这样做。您是否希望向您的老板解释您的网站发生故障或者您需要撤回已发布的数据分析,因为您采用了这个糟糕的想法并使用它?

答案 1 :(得分:3)

这可能算作你不想要的“附加名称”,但是我使用了元类中的字典来实现它,其中键是类。元类的__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。

答案 2 :(得分:-1)

  

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

你所要求的是一个矛盾:如果你的例子有效,那么__class__.__dict__['x']将是一个额外的名字"对于属性。显然,我们需要更具体的定义"附加名称。"但是要想出这个定义,我们需要知道你想要完成什么(注意:以下目标相互排斥,所以你可能想要做所有这些事情):

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