Python 定义 __slots__ 的元类使 __slots__ 只读

Python Metaclass defining __slots__ makes __slots__ readonly

在下面的示例中,我尝试创建一个 python metaclass,它是我的 class,具有 __slots__ 和默认值。

class Meta(type):
    def __new__(cls, name, bases, dictionary, defaults):
        dictionary['__slots__'] = list(defaults.keys())
        obj = super().__new__(cls, name, bases, dictionary)
        return obj
    def __init__(self, name, bases, dictionary, defaults):
        for s in defaults:
            setattr(self, s, defaults[s])

                        
class A(metaclass = Meta, defaults = {'a':123, 'b':987}):
    pass

实例化classA,得到如下结果:

a = A()
>>> dir (a)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'a', 'b']

-> 确定

>>> a.c = 500
Traceback (most recent call last):
  File "<pyshell#87>", line 1, in <module>
    a.c = 500
AttributeError: 'A' object has no attribute 'c'

-> 确定

>>> a.b = 40
Traceback (most recent call last):
  File "<pyshell#88>", line 1, in <module>
    a.b = 40
AttributeError: 'A' object attribute 'b' is read-only

-> 不正常,预计 a.b 可读写

A 你可以看到 metaclass Meta 正确创建了 __slots__ 并正确设置了默认值,但不幸的是,由于某些我不知道的原因,开槽属性被设为只读不明白。 是否可以从 metaclass Meta?

中获取开槽 read/write 属性

问题是在 Meta.__init__ 中设置属性的代码在 class 本身中更改了它。问题是 class 中的默认变量(本例中的“a”和“b”默认值)是特殊的描述符,用于处理 实例中的槽值分配 创建的 class (您的示例中的对象“a”)。描述符被覆盖,无法再工作。 (它们变成“只读 class 属性”确实是一种特殊的副作用 - 我将调查这是记录在案的还是故意的,或者只是未定义的行为)

尽管如此,您需要一种方法来设置在实例化对象后使值在槽变量中可用。

一个明显的方法是将 Meta.__init__ 中的逻辑转移到基础 class __init__ 方法,并在那里设置值(附加 defaults dict 到 class 本身)。然后任何调用 super().__init__() 的子classes 都会有它。

如果您不想或不能这样做,您可以将代码放入元class 中以在每个 class 中注入一个 __init__,包装原始的 __init__ 如果有的话(并处理所有可能的情况,例如:没有 __init__ ,已经在父 class 中包装了 __init__ 等...) - 这可以做到,如果您选择这样做,我可以提供一些示例代码。

(update:再想一想,代码可以设置在 metaclass __call__ 方法上,而不是所有这些恶意行为,并且完全覆盖默认 type.__call__,因此默认值赋值发生在 class' __init__ 被调用之前)




class Meta(type):
    def __new__(mcls, name, bases, dictionary, defaults):
        dictionary['__slots__'] = list(defaults)
        dictionary["_defaults"] = defaults
        return super().__new__(mcls, name, bases, dictionary)
        
    def __call__(cls, *args, **kw):
        """Replaces completly the mechanism that makes  `__new__` and 
        `__init__` being called, adding a new step between the two calls
        """
        instance = cls.__new__(cls, *args, **kw)
        for k, v in instance._defaults.items():
            setattr(instance, k, v)
        instance.__init__(*args, **kw)
        return instance
                        
class A(metaclass = Meta, defaults = {'a':123, 'b':987}):
    def __init__(self):
        print (f"I can see the default values of a and b: {(self.a, self.b)}")
        

并且有效:


In [51]: A()                                                                                                                              
I can see the default values of a and b: (123, 987)
Out[51]: <__main__.A at 0x7f093cfeb820>

In [52]: a = A()                                                                                                                          
I can see the default values of a and b: (123, 987)

In [53]: a.c = 500                                                                                                                        
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-53-ce3d946a718e> in <module>
----> 1 a.c = 500

AttributeError: 'A' object has no attribute 'c'

In [54]: a.b                                                                                                                              
Out[54]: 987

In [55]: a.b = 1000                                                                                                                       

In [56]: a.b                                                                                                                              
Out[56]: 1000

另一种方法是创建知道默认值的特殊描述符。更改带前缀的变量名称(例如“_”),并使用这些描述符来访问它们。这有点直截了当,尽管它比编写 metaclass __call__ 更复杂,但您的优势在于能够在描述符本身上放置额外的保护代码(例如:拒绝赋值默认值的类型不同)

PREFIX = "_"

class DefaultDescriptor:

    def __init__(self, name, default):
        self.name = name
        self.default = default
    def __get__(self, instance, owner):
        if instance is None: 
            return self
            # or, if you want the default value to be visible as a class attribute:
            # return self.default 
        return getattr(instance, PREFIX + self.name, self.default)
    
    def __set__(self, instance, value):
        setattr(instance, PREFIX + self.name, value)
        


class Meta(type):
    def __new__(mcls, name, bases, dictionary, defaults):
        dictionary['__slots__'] = [PREFIX + key for key in defaults]
        cls = super().__new__(mcls, name, bases, dictionary)
        for key, value in defaults.items():
            setattr(cls, key, DefaultDescriptor(key, value))
        return cls
    
                        
class A(metaclass = Meta, defaults = {'a':123, 'b':987}):
    pass

在 REPL 上:

In [37]: a = A()                                                                                                                          

In [38]: a.c = 500                                                                                                                        
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-38-ce3d946a718e> in <module>
----> 1 a.c = 500

AttributeError: 'A' object has no attribute 'c'

In [39]: a.b                                                                                                                              
Out[39]: 987

In [40]: a.b = 1000                                                                                                                       

In [41]: a.b                                                                                                                              
Out[41]: 1000