【问题标题】:How can I force subclasses to have __slots__?如何强制子类具有 __slots__?
【发布时间】:2019-10-27 23:31:23
【问题描述】:

我有 __slots__ 的课:

class A:
    __slots__ = ('foo',)

如果我创建一个子类而不指定__slots__,则子类将有一个__dict__

class B(A):
    pass

print('__dict__' in dir(B))  # True

有什么方法可以防止B 拥有__dict__ 而无需设置__slots__ = ()

【问题讨论】:

  • 背景:我将实例化 很多A 对象,所以我使用 __slots__ 来减少内存消耗。 A 也位于具有许多 mixin 和子类的大型类层次结构的顶部,因此我想避免在每个子类中编写 __slots__ = ()
  • @DeveshKumarSingh 我不明白这如何回答我的问题 - 我了解 __slots__ 如何与继承交互,我想找到一种方法来改变它的工作方式。
  • 如果你只想腾出内存,在实例化时像B.__dict__ = ()一样动态清空__dict__是否是一个解决方案?
  • @MSeifert 不,可能有一些子类添加了新的实例属性。这个问题只是关于那些根本没有定义__slots__ 的子类——这些类应该默认将__slots__ 设置为()确实 定义__slots__ 的类应该不理会。也不需要退出机制,因为它已经存在:需要 dict 的类可以设置 __slots__ = ('__dict__',)

标签: python class inheritance metaprogramming slots


【解决方案1】:

@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__ 插槽。

【讨论】:

  • 已经在对该问题的评论中说过,但我认为值得指出的是 del __slots__ 并不是给你的班级一个 dict 的唯一方法:你也可以显式地创建一个插槽对于字典,例如__slots__ = ('__dict__',)。这两个选项之间存在细微差别:del __slots__ 不仅会为您的班级提供__dict__,还会为您的班级提供__weakref__ 插槽。
【解决方案2】:

像这样的元类和the __prepare__() hook怎么样?

import sys


class InheritSlots(type):
    def __prepare__(name, bases, **kwds):
        # this could combine slots from bases, I guess, and walk the base hierarchy, etc
        for base in bases:
            if base.__slots__:
                kwds["__slots__"] = base.__slots__
                break
        return kwds


class A(metaclass=InheritSlots):
    __slots__ = ("foo", "bar", "quux")


class B(A):
    pass


assert A.__slots__
assert B.__slots__ == A.__slots__
assert "__dict__" not in dir(A)
assert "__dict__" not in dir(B)

print(sys.getsizeof(A()))
print(sys.getsizeof(B()))

出于某种原因,这仍然会打印64, 88——也许继承类的实例总是比基类本身重一点?

【讨论】:

  • 子类not中的槽不应该和它们的父类有相同的东西吗?来自文档:“除非他们还定义了 __slots__(它应该只包含任何 additional 插槽的名称)。”
  • 好主意,但我不喜欢实施。就像 aneroid 所说,子类不应该复制其父类的 __slots__ - 正确的解决方案是将其设置为空元组。而且我认为将关键字参数转换为类属性的__prepare__ 方法非常值得怀疑。我选择在元类的__new__ 中实现逻辑,而是使用简单的cls_dict.setdefault('__slots__', ())
  • 这显然是__prepare__ 应该如何工作的,kwds 的事情。 (在此之前我实际上没有使用过__prepare__,我只是在手册和相关的PEP中查找了它。)
  • 嗯,我不敢说实话。但如果是这样,我就不会手动进行 - 我只会拨打super().__prepare__ 电话。
猜你喜欢
  • 2019-08-13
  • 1970-01-01
  • 2011-08-01
  • 2015-07-18
  • 1970-01-01
  • 2012-12-20
  • 1970-01-01
  • 1970-01-01
  • 2013-11-03
相关资源
最近更新 更多