Как я могу заставить подклассы иметь __slots__? - PullRequest
3 голосов
/ 13 июня 2019

У меня есть класс с __slots__:

class A:
    __slots__ = ('foo',)

Если я создаю подкласс без указания __slots__, у подкласса будет __dict__:

class B(A):
    pass

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

Есть ли способ запретить B иметь __dict__ без необходимости устанавливать __slots__ = ()?

Ответы [ 2 ]

4 голосов
/ 13 июня 2019

Ответ @ AKX почти правильный.Я думаю, __prepare__ и метакласс действительно способ решить эту проблему довольно легко.

Просто подведем итог:

  • Если пространство имен класса содержит ключ __slots__ после выполнения тела класса, тогда класс будет использовать __slots__ вместо __dict__.
  • Можно ввести имена в пространство имен класса до того, как тело класса будет выполнено с использованием __prepare__.

Так что, если мы просто вернемсловарь, содержащий ключ '__slots__' из __prepare__, тогда класс (если ключ '__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__",

, который проходит тесты, упомянутые в AKZs, отвечает:

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__.

0 голосов
/ 13 июня 2019

Как насчет метакласса, подобного этому, и __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 - возможно, экземпляры унаследованного класса всегданемного тяжелее, чем сам базовый класс?

...