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
在下面的示例中,我尝试创建一个 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
?
问题是在 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