如何解决定义 __slots__ 的 Python 元类使 __slots__ 只读
在下面的示例中,我尝试创建一个 python 元类,它是我的类,具有 __slots__
和默认值。
class Meta(type):
def __new__(cls,name,bases,dictionary,defaults):
dictionary['__slots__'] = list(defaults.keys())
obj = super().__new__(cls,dictionary)
return obj
def __init__(self,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']
-> 好的
>>> 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>",in <module>
a.b = 40
AttributeError: 'A' object attribute 'b' is read-only
-> 不行,预计 a.b
是可读写的
您可以看到元类 Meta
正确创建了 __slots__
并正确设置了默认值,但不幸的是,由于某些我不明白的原因,插槽属性被设为只读。
是否可以从元类 Meta
中获取槽读/写属性?
解决方法
问题是在 Meta.__init__
中设置属性的代码在类本身中更改了它。问题在于类中的默认变量(在本例中为“a”和“b”默认值)是特殊的描述符,用于处理创建的类的实例中的槽值分配(您的示例中的对象“a”)。描述符被覆盖并且无法再工作。
(它们变成“只读类属性”确实是一个奇特的副作用——我会调查这是记录在案还是故意的,或者只是未定义的行为)
尽管如此,您需要的是一种设置方法,以便在对象实例化后使槽变量中的值可用。
一种显而易见的方法是将 Meta.__init__
中的逻辑转移到基类 __init__
方法,并在那里设置值(将 defaults
dict 附加到类本身)。然后任何调用 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,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,**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,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,value):
setattr(instance,value)
class Meta(type):
def __new__(mcls,defaults):
dictionary['__slots__'] = [PREFIX + key for key in defaults]
cls = super().__new__(mcls,dictionary)
for key,value in defaults.items():
setattr(cls,key,DefaultDescriptor(key,value))
return cls
class A(metaclass = Meta,'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
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。