简体   繁体   English

如何强制子类具有__slots__?

[英]How can I force subclasses to have __slots__?

I have a class with __slots__ : 我有一个__slots__类:

class A:
    __slots__ = ('foo',)

If I create a subclass without specifying __slots__ , the subclass will have a __dict__ : 如果我在没有指定__slots__情况下创建子类,则子类将具有__dict__

class B(A):
    pass

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

Is there any way to prevent B from having a __dict__ without having to set __slots__ = () ? 有没有办法阻止B拥有__dict__而不必设置__slots__ = ()

The answer of @AKX is almost correct. @AKX的答案几乎是正确的。 I think __prepare__ and a metaclass is indeed the way this can be solved quite easily. 我认为__prepare__和元类确实是很容易解决的问题。

Just to recap: 回顾一下:

  • If the namespace of the class contains a __slots__ key after the class body is executed then the class will use __slots__ instead of __dict__ . 如果类的名称空间在执行类主体后包含__slots__键,则该类将使用__slots__而不是__dict__
  • One can inject names into the namespace of the class before the class body is executed by using __prepare__ . 使用__prepare__执行类主体之前 ,可以将名称注入类的名称空间。

So if we simply return a dictionary containing the key '__slots__' from __prepare__ then the class will (if the '__slots__' key isn't removed again during the evaluation of the class body) use __slots__ instead of __dict__ . 因此,如果我们只是从__prepare__返回一个包含键'__slots__'的字典,那么该类将(如果在评估类体时没有再次删除'__slots__'键)使用__slots__而不是__dict__ Because __prepare__ just provides the initial namespace one can easily override the __slots__ or remove them again in the class body. 因为__prepare__只提供初始命名空间,所以可以轻松覆盖__slots__或在类体中再次删除它们。

So a metaclass that provides __slots__ by default would look like this: 因此,默认情况下提供__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

So every class and subclass with this metaclass will (by default) have an empty __slots__ in their namespace and thus create a "class with slots" (except the __slots__ are removed on purpose). 因此,具有此元类的每个类和子类(默认情况下)在其命名空间中都有一个空的__slots__ ,从而创建一个“带有插槽的类”(除了__slots__之外,它们会被删除)。

Just to illustrate how this would work: 只是为了说明这是如何工作的:

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__",

Which passes the tests mentioned in AKZs answer: 通过了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)

And to verify that it works as expected: 并验证它是否按预期工作:

# 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

As pointed out in a comment (by Aran-Fey ) there is a difference between del __slots__ and adding __dict__ to the __slots__ : 正如在注释中指出(由阿兰-的Fcγ )之间有一个差del __slots__和添加__dict____slots__

There's a minor difference between the two options: del __slots__ will give your class not only a __dict__ , but also a __weakref__ slot. 这两个选项之间存在细微差别: del __slots__会为您的类提供__dict__ ,还会为__weakref__一个槽。

How about a metaclass like this and the __prepare__() hook ? 像这样的元类和__prepare__()钩子怎么样?

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()))

For some reason, this still does print 64, 88 – maybe an inherited class's instances are always a little heavier than the base class itself? 出于某种原因,这仍然打印64, 88 - 也许一个继承类的实例总是比基类本身重一点?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM