Python元类定义__slots__会使__slots__只读。

3
在下面的示例中,我尝试创建一个Python元类,用__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

实例化类 A,我得到了以下结果:

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

-> OK

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

-> OK

>>> 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是可读写的

正如您所看到的,元类Meta正确创建了__slots__并正确设置了默认值,但不幸的是,由于某些我不理解的原因,这些属性被设置为只读。 是否可能从元类Meta获取具有读/写功能的slotted属性?

1个回答

3
问题在于,在Meta.__init__中设置属性的代码会改变类本身。问题在于类中的默认变量(在这种情况下为"a"和"b"默认值)是特殊描述符,用于处理创建的类实例(例如您示例中的对象"a")中的插槽值分配。这些描述符被覆盖,不能再起作用。(事实上,这是一个奇怪的副作用,它们变成了“只读类属性” - 我将调查是否有文档记录或是故意为之,还是仅仅是未定义的行为)
尽管如此,您需要一种方法来在对象实例化后使值在插槽变量中可用。
一个明显的方法是将Meta.__init__中的逻辑转移到一个基类__init__方法中,并在那里设置值(将defaults字典附加到类本身)。然后,调用super().__init__()的任何子类都将拥有它。
如果您不想或无法这样做,您需要在元类中放置代码以注入每个类中的__init__,包装原始的__init__(如果有的话),并处理所有可能的情况,例如:没有__init__,已经在父类中有一个包装的__init__等——这是可以完成的,如果您选择这种方法,我可以提供一些示例代码。
(更新:经过再次考虑,与其进行所有这些操作,还不如将代码设置为元类__call__方法,并完全覆盖默认的type.__call__,以便在调用类的__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

另一种方法是创建特殊的描述符,它们将知道默认值。更改插槽变量名称并添加前缀(例如“_”),然后使用这些描述符来访问它们。这有点直接了当,虽然它比编写元类__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

哇,这是一件需要逐步跟进的事情。目前我还没有完全理解所有内容。不过,看起来这是一个非常成熟和有前景的方法。 - undefined

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