@AKX 的答案几乎是正确的。我认为__prepare__ 和元类确实是可以很容易解决的方法。
回顾一下:
- 如果类的命名空间在类主体执行后包含
__slots__ 键,则该类将使用__slots__ 而不是__dict__。
- 可以在使用
__prepare__ 执行类主体之前将名称注入类的命名空间。
因此,如果我们只是从 __prepare__ 返回包含键 '__slots__' 的字典,那么该类将(如果在评估类主体期间未再次删除 '__slots__' 键)使用 __slots__ 而不是__dict__。
因为__prepare__ 只是提供了初始命名空间,所以可以轻松地覆盖__slots__ 或在类主体中再次删除它们。
因此,默认情况下提供__slots__ 的元类如下所示:
class ForceSlots(type):
@classmethod
def __prepare__(metaclass, name, bases, **kwds):
# calling super is not strictly necessary because
# type.__prepare() simply returns an empty dict.
# But if you plan to use metaclass-mixins then this is essential!
super_prepared = super().__prepare__(metaclass, name, bases, **kwds)
super_prepared['__slots__'] = ()
return super_prepared
因此,每个具有此元类的类和子类(默认情况下)在其命名空间中都会有一个空的__slots__,从而创建一个“带插槽的类”(__slots__ 被故意删除除外)。
只是为了说明这是如何工作的:
class A(metaclass=ForceSlots):
__slots__ = "a",
class B(A): # no __dict__ even if slots are not defined explicitly
pass
class C(A): # no __dict__, but provides additional __slots__
__slots__ = "c",
class D(A): # creates normal __dict__-based class because __slots__ was removed
del __slots__
class E(A): # has a __dict__ because we added it to __slots__
__slots__ = "__dict__",
通过 AKZ 回答中提到的测试:
assert "__dict__" not in dir(A)
assert "__dict__" not in dir(B)
assert "__dict__" not in dir(C)
assert "__dict__" in dir(D)
assert "__dict__" in dir(E)
并验证它是否按预期工作:
# A has slots from A: a
a = A()
a.a = 1
a.b = 1 # AttributeError: 'A' object has no attribute 'b'
# B has slots from A: a
b = B()
b.a = 1
b.b = 1 # AttributeError: 'B' object has no attribute 'b'
# C has the slots from A and C: a and c
c = C()
c.a = 1
c.b = 1 # AttributeError: 'C' object has no attribute 'b'
c.c = 1
# D has a dict and allows any attribute name
d = D()
d.a = 1
d.b = 1
d.c = 1
# E has a dict and allows any attribute name
e = E()
e.a = 1
e.b = 1
e.c = 1
正如评论(Aran-Fey)中指出的那样,del __slots__ 和将 __dict__ 添加到 __slots__ 之间是有区别的:
这两个选项之间存在细微差别:del __slots__ 不仅会为您的班级提供__dict__,还会为您的班级提供__weakref__ 插槽。