使用__slots__?

Python中#0的目的是什么-特别是关于我何时想要使用它,何时不想使用它?

331491 次浏览

如果您要实例化同一类的大量(数百、数千)对象,您可能希望使用__slots____slots__仅作为内存优化工具存在。

强烈建议不要使用__slots__来限制属性的创建。

使用__slots__酸洗对象不适用于默认(最古老的)酸洗协议;有必要指定更高的版本。

Python的其他一些自省功能也可能受到不利影响。

引用雅各布·哈伦

正确使用__slots__是为了节省对象的空间。而不是一个动态字典,允许随时向对象添加属性,有一个静态结构,不允许在创建后添加。[这种__slots__的使用消除了每个对象一个字典的开销。]虽然这有时是一个有用的优化,但它完全是如果Python解释器足够动态,那么它就不必要了只有在实际添加对象时才需要字典。

不幸的是,插槽有副作用。它们改变了那些有槽的东西会被控制狂滥用和静态输入的小弟弟。这很糟糕,因为控制狂应该滥用元类,静态类型的weenies应该被滥用装饰器,因为在Python中,应该只有一种明显的方式来做某事。

使CPython足够聪明以在没有__slots__的情况下节省空间是一个主要的问题这可能就是为什么它不在P3k的更改列表中的原因。

你有-基本上-没有使用__slots__

当你认为你可能需要__slots__的时候,你实际上想使用轻量级轻量级的设计模式。这些是你不再想使用纯Python对象的情况。相反,你需要一个类似Python对象的包装器来包装数组、结构或numpy数组。

class Flyweight(object):
def get(self, theData, index):return theData[index]
def set(self, theData, index, value):theData[index]= value

类包装器没有属性-它只是提供作用于底层数据的方法。方法可以简化为类方法。事实上,它可以简化为仅在底层数据数组上操作的函数。

每个python对象都有一个__dict__属性,这是一个包含所有其他属性的字典。例如,当你键入self.attr时,python实际上正在执行self.__dict__['attr']。正如你所想象的那样,使用字典来存储属性需要一些额外的空间和时间来访问它。

但是,当您使用__slots__时,为该类创建的任何对象都不会具有__dict__属性。相反,所有属性访问都直接通过指针完成。

因此,如果想要一个C风格的结构而不是一个成熟的类,您可以使用__slots__来压缩对象的大小并减少属性访问时间。一个很好的例子是包含属性x和y的Point类。如果你有很多点,你可以尝试使用__slots__来节省一些内存。

插槽对于库调用非常有用,可以消除在进行函数调用时的“命名方法分派”。这在SWIG留档中有所提及。对于希望使用插槽减少常用调用函数的函数开销的高性能库来说,速度要快得多。

现在这可能与OP的问题没有直接关系。它与构建扩展的关系大于在对象上使用插槽语法的关系。但它确实有助于完成槽的使用以及它们背后的一些推理。

类实例的属性有3个属性:实例、属性名称和属性值。

常规属性访问中,实例充当字典,属性的名称充当该字典中查找值的键。

实例(属性)-->值

__slots__访问中,属性的名称充当字典,实例充当字典中查找值的键。

属性(实例)-->值

轻量级模式中,属性的名称充当字典,值充当该字典中查找实例的键。

实例类型

在Python中,__slots__的目的是什么,应该避免哪些情况?

TLDR:

特殊属性#0允许您显式声明您希望对象实例具有哪些实例属性,并给出预期结果:

  1. 更快属性访问。
  2. 节省空间内存。

节省的空间来自

  1. 将值引用存储在插槽中而不是__dict__
  2. 拒绝#0#1创建,如果父类拒绝它们并且您声明__slots__

快速警告

小警告,您应该只在继承树中声明一次特定的槽。例如:

class Base:__slots__ = 'foo', 'bar'
class Right(Base):__slots__ = 'baz',
class Wrong(Base):__slots__ = 'foo', 'bar', 'baz'        # redundant foo and bar

当你弄错了(它可能应该)时,Python不会反对,否则问题可能不会表现出来,但是你的对象会占用比它们应该占用的更多的空间。Python 3.8:

>>> from sys import getsizeof>>> getsizeof(Right()), getsizeof(Wrong())(56, 72)

这是因为Base的槽描述符有一个与错误的槽分开的槽。这通常不应该出现,但它可能:

>>> w = Wrong()>>> w.foo = 'foo'>>> Base.foo.__get__(w)Traceback (most recent call last):File "<stdin>", line 1, in <module>AttributeError: foo>>> Wrong.foo.__get__(w)'foo'

最大的警告是多重继承-多个“具有非空槽的父类”不能组合。

为了适应这一限制,遵循最佳实践:除一个或所有父类抽象之外,将它们的具体类和新的具体类共同继承的所有父类抽象考虑在内——给抽象空槽(就像标准库中的抽象基类一样)。

有关示例,请参阅下面的多重继承部分。

职位要求:

  • 要将__slots__中命名的属性实际存储在插槽中而不是__dict__中,类必须从object继承(在Python 3中是自动的,但在Python 2中必须是显式的)。

  • 要防止创建__dict__,您必须从object继承,继承中的所有类都必须声明__slots__,并且它们都不能有'__dict__'条目。

有很多细节,如果你想继续阅读。

为什么使用__slots__:更快的属性访问。

Python的创建者,Guido van Rossum,国家,他实际上创建了__slots__以更快地访问属性。

证明显着更快的访问是微不足道的:

import timeit
class Foo(object): __slots__ = 'foo',
class Bar(object): pass
slotted = Foo()not_slotted = Bar()
def get_set_delete_fn(obj):def get_set_delete():obj.foo = 'foo'obj.foodel obj.fooreturn get_set_delete

>>> min(timeit.repeat(get_set_delete_fn(slotted)))0.2846834529991611>>> min(timeit.repeat(get_set_delete_fn(not_slotted)))0.3664822799983085

在Ubuntu上的Python 3.5中,时隙访问几乎快了30%。

>>> 0.3664822799983085 / 0.28468345299916111.2873325658284342

在Windows上的Python 2中,我测量它快了大约15%。

为什么使用__slots__:节省内存

__slots__的另一个目的是减少每个对象实例占用的内存空间。

我自己对留档的贡献清楚地说明了这背后的原因

使用__dict__节省的空间可能很大。

SQLAlchemy属性节省了大量内存__slots__

为了验证这一点,使用UbuntuLinux上Python 2.7的Anaconda发行版,带有guppy.hpy(又名hepy)和sys.getsizeof,没有声明__slots__的类实例的大小是64个字节。没有包括__dict__。再次感谢Python的懒惰评估,__dict__显然在被引用之前不会被调用,但没有数据的类通常是无用的。调用时,__dict__属性至少还有280个字节。

相比之下,将__slots__声明为()(无数据)的类实例只有16个字节,总共56个字节,一个项目在插槽中,64个有两个。

对于64位Python,我在Python 2.7和3.6中说明了以字节为单位的内存消耗,对于__slots____dict__(未定义槽),对于字典在3.6中增长的每个点(0、1和2属性除外):

       Python 2.7             Python 3.6attrs  __slots__  __dict__*   __slots__  __dict__* | *(no slots defined)none   16         56 + 272†   16         56 + 112† | †if __dict__ referencedone    48         56 + 272    48         56 + 112two    56         56 + 272    56         56 + 112six    88         56 + 1040   88         56 + 15211     128        56 + 1040   128        56 + 24022     216        56 + 3344   216        56 + 40843     384        56 + 3344   384        56 + 752

因此,尽管Python 3中的dicts较小,但我们看到__slots__可以很好地扩展实例以节省我们的内存,这是您想要使用__slots__的主要原因。

只是为了我笔记的完整性,请注意,在Python 2中类的命名空间中,每个插槽的一次性成本为64个字节,在Python 3中为72个字节,因为插槽使用像属性这样的数据描述符,称为“成员”。

>>> Foo.foo<member 'foo' of 'Foo' objects>>>> type(Foo.foo)<class 'member_descriptor'>>>> getsizeof(Foo.foo)72

演示__slots__

要拒绝创建__dict__,您必须子类object。Python 3中的所有子类object,但在Python 2中,您必须明确:

class Base(object):__slots__ = ()

现在:

>>> b = Base()>>> b.a = 'a'Traceback (most recent call last):File "<pyshell#38>", line 1, in <module>b.a = 'a'AttributeError: 'Base' object has no attribute 'a'

或者子类另一个定义__slots__的类

class Child(Base):__slots__ = ('a',)

现在:

c = Child()c.a = 'a'

但是:

>>> c.b = 'b'Traceback (most recent call last):File "<pyshell#42>", line 1, in <module>c.b = 'b'AttributeError: 'Child' object has no attribute 'b'

要允许在子类化开槽对象时创建__dict__,只需将'__dict__'添加到__slots__(请注意,插槽是有序的,您不应该重复父类中已经存在的插槽):

class SlottedWithDict(Child):__slots__ = ('__dict__', 'b')
swd = SlottedWithDict()swd.a = 'a'swd.b = 'b'swd.c = 'c'

>>> swd.__dict__{'c': 'c'}

或者你甚至不需要在你的子类中声明__slots__,你仍然会使用父类的插槽,但不限制__dict__的创建:

class NoSlots(Child): passns = NoSlots()ns.a = 'a'ns.b = 'b'

还有:

>>> ns.__dict__{'b': 'b'}

但是,__slots__可能会导致多重继承问题:

class BaseA(object):__slots__ = ('a',)
class BaseB(object):__slots__ = ('b',)

因为从具有两个非空槽的父类创建子类失败:

>>> class Child(BaseA, BaseB): __slots__ = ()Traceback (most recent call last):File "<pyshell#68>", line 1, in <module>class Child(BaseA, BaseB): __slots__ = ()TypeError: Error when calling the metaclass basesmultiple bases have instance lay-out conflict

如果你遇到这个问题,你可以只需从父级中删除__slots__,或者如果你可以控制父级,给它们空槽,或者重构为抽象:

from abc import ABC
class AbstractA(ABC):__slots__ = ()
class BaseA(AbstractA):__slots__ = ('a',)
class AbstractB(ABC):__slots__ = ()
class BaseB(AbstractB):__slots__ = ('b',)
class Child(AbstractA, AbstractB):__slots__ = ('a', 'b')
c = Child() # no problem!

'__dict__'添加到__slots__以获得动态分配:

class Foo(object):__slots__ = 'bar', 'baz', '__dict__'

现在:

>>> foo = Foo()>>> foo.boink = 'boink'

因此,在插槽中使用'__dict__',我们失去了一些大小优势,因为具有动态分配并且仍然具有我们期望的名称的插槽。

当您从未开槽的对象继承时,当您使用__slots__时,您会得到同样的语义学-__slots__中的名称指向开槽的值,而任何其他值都放在实例的__dict__中。

避免__slots__,因为您希望能够动态添加属性实际上不是一个好的理由-如果需要,只需将"__dict__"添加到__slots__即可。

如果您需要该功能,您可以类似地将__weakref__显式添加到__slots__

子类化namedtuple时设置为空元组:

namedtuple内置的不可变实例非常轻量级(本质上是元组的大小),但要获得好处,如果你对它们进行子类化,你需要自己做:

from collections import namedtupleclass MyNT(namedtuple('MyNT', 'bar baz')):"""MyNT is an immutable and lightweight object"""__slots__ = ()

用法:

>>> nt = MyNT('bar', 'baz')>>> nt.bar'bar'>>> nt.baz'baz'

尝试分配一个意外的属性会引发AttributeError,因为我们已经阻止了__dict__的创建:

>>> nt.quux = 'quux'Traceback (most recent call last):File "<stdin>", line 1, in <module>AttributeError: 'MyNT' object has no attribute 'quux'

可以通过离开__slots__ = ()允许__dict__创建,但您不能将非空__slots__与元组的子类型一起使用。

最大的警告:多重继承

即使非空插槽对于多个父节点相同,它们也不能一起使用:

class Foo(object):__slots__ = 'foo', 'bar'class Bar(object):__slots__ = 'foo', 'bar' # alas, would work if empty, i.e. ()
>>> class Baz(Foo, Bar): passTraceback (most recent call last):File "<stdin>", line 1, in <module>TypeError: Error when calling the metaclass basesmultiple bases have instance lay-out conflict

在父级中使用空__slots__似乎提供了最大的灵活性,允许孩子选择阻止或允许(通过添加'__dict__'来获得动态赋值,请参阅上面的部分)创建一个#2

class Foo(object): __slots__ = ()class Bar(object): __slots__ = ()class Baz(Foo, Bar): __slots__ = ('foo', 'bar')b = Baz()b.foo, b.bar = 'foo', 'bar'

你不有插槽-所以如果你添加它们,稍后删除它们,它不应该造成任何问题。

在这里冒险了:如果你正在编写Mixins或使用抽象基类,它们不打算被实例化,那么在这些父类中的空__slots__似乎是子类灵活性的最佳方式。

为了演示,首先,让我们创建一个类,其中包含我们希望在多重继承下使用的代码

class AbstractBase:__slots__ = ()def __init__(self, a, b):self.a = aself.b = bdef __repr__(self):return f'{type(self).__name__}({repr(self.a)}, {repr(self.b)})'

我们可以通过继承和声明预期的插槽直接使用上述内容:

class Foo(AbstractBase):__slots__ = 'a', 'b'

但我们不关心这个,这是微不足道的单继承,我们需要另一个我们也可以继承的类,也许有一个嘈杂的属性:

class AbstractBaseC:__slots__ = ()@propertydef c(self):print('getting c!')return self._c@c.setterdef c(self, arg):print('setting c!')self._c = arg

现在,如果两个基地都有非空插槽,我们不能做下面的事情。(事实上,如果我们愿意,我们可以给出AbstractBase个非空插槽a和b,并将它们排除在下面的声明之外-将它们留在里面是错误的):

class Concretion(AbstractBase, AbstractBaseC):__slots__ = 'a b _c'.split()

现在我们通过多重继承获得了两者的功能,并且仍然可以拒绝__dict____weakref__实例化:

>>> c = Concretion('a', 'b')>>> c.c = csetting c!>>> c.cgetting c!Concretion('a', 'b')>>> c.d = 'd'Traceback (most recent call last):File "<stdin>", line 1, in <module>AttributeError: 'Concretion' object has no attribute 'd'

其他避免插槽的情况:

  • 当您想对另一个没有它们的类执行__class__赋值时(并且您不能添加它们),除非插槽布局相同。(我对了解谁在这样做以及为什么很感兴趣。)
  • 如果您想对long、tuple或str等可变长度内置项进行子类化,并且想向它们添加属性,请避免使用它们。
  • 如果您坚持通过实例变量的类属性提供默认值,请避免使用它们。

你也许可以从__slots__留档(3.7开发文档是最新的)的其余部分中梳理出进一步的警告,我最近对此做出了重大贡献。

对其他答案的批评

目前的顶级答案引用了过时的信息,并且在一些重要方面非常手舞足蹈,并且错过了标记。

不要“仅在实例化大量对象时使用__slots__

我引述如下:

“如果您要实例化同一类的大量(数百、数千)对象,您可能希望使用__slots__。”

例如,来自collections模块的抽象基类没有实例化,但为它们声明了__slots__

为啥?

如果用户希望拒绝__dict____weakref__的创建,这些东西在父类中必须不可用。

__slots__有助于创建接口或混合时的可重用性。

诚然,许多Python用户不是为了可重用性而编写的,但是当你这样做的时候,可以选择拒绝不必要的空间使用是很有价值的。

__slots__不会破坏酸洗

当酸洗一个开槽的物体时,你可能会发现它抱怨一个误导性的TypeError

>>> pickle.loads(pickle.dumps(f))TypeError: a class that defines __slots__ without defining __getstate__ cannot be pickled

这实际上是不正确的。此消息来自最古老的协议,这是默认的。您可以使用-1参数选择最新的协议。在Python 2.7中,这将是2(在2.3中引入),在3.6中是4

>>> pickle.loads(pickle.dumps(f, -1))<__main__.Foo object at 0x1129C770>

在Python 2.7中:

>>> pickle.loads(pickle.dumps(f, 2))<__main__.Foo object at 0x1129C770>

在python3.6

>>> pickle.loads(pickle.dumps(f, 4))<__main__.Foo object at 0x1129C770>

所以我会记住这一点,因为这是一个已经解决的问题。

批评(直到2016年10月2日)接受的答案

第一段一半是简短的解释,一半是预测。这是唯一真正回答问题的部分

__slots__的正确使用是节省对象中的空间。不是有一个允许随时向对象添加属性的动态字典,而是有一个静态结构,它不允许在创建后添加。这为每个使用槽的对象节省了一个字典的开销

后半部分是一厢情愿,离题:

虽然这有时是一个有用的优化,但如果Python解释器足够动态,以至于它只在实际添加对象时才需要字典,则完全没有必要。

Python实际上做了类似的事情,只在访问时创建__dict__,但创建大量没有数据的对象是相当荒谬的。

第二段过于简单化,忽略了避免__slots__的实际原因。下面是没有避免插槽的真正原因(对于实际原因,请参阅我上面的其余答案):

它们改变了具有槽的对象的行为,这种方式可能会被控制狂和静态类型小人滥用。

然后,它继续讨论使用Python实现这个反常目标的其他方法,而不是讨论与__slots__有关的任何事情。

第三段则更像是一厢情愿的想法,大部分内容都不符合作者的要求,为网站批评者提供了弹药。

内存使用证据

创建一些普通对象和开槽对象:

>>> class Foo(object): pass>>> class Bar(object): __slots__ = ()

实例化一百万个:

>>> foos = [Foo() for f in xrange(1000000)]>>> bars = [Bar() for b in xrange(1000000)]

检查guppy.hpy().heap()

>>> guppy.hpy().heap()Partition of a set of 2028259 objects. Total size = 99763360 bytes.Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)0 1000000  49 64000000  64  64000000  64 __main__.Foo1     169   0 16281480  16  80281480  80 list2 1000000  49 16000000  16  96281480  97 __main__.Bar3   12284   1   987472   1  97268952  97 str...

访问常规对象及其__dict__并再次检查:

>>> for f in foos:...     f.__dict__>>> guppy.hpy().heap()Partition of a set of 3028258 objects. Total size = 379763480 bytes.Index  Count   %      Size    % Cumulative  % Kind (class / dict of class)0 1000000  33 280000000  74 280000000  74 dict of __main__.Foo1 1000000  33  64000000  17 344000000  91 __main__.Foo2     169   0  16281480   4 360281480  95 list3 1000000  33  16000000   4 376281480  99 __main__.Bar4   12284   0    987472   0 377268952  99 str...

这与Python的历史一致,从在Python 2.2中统一类型和类开始

如果你对内置类型进行子类化,额外的空间会自动添加到实例中以容纳__dict____weakrefs__。(虽然__dict__在你使用它之前不会初始化,所以你不必担心你创建的每个实例的空字典占用的空间。)如果你不需要这个额外的空间,你可以在你的类中添加短语“__slots__ = []”。

除了其他答案,这里有一个使用__slots__的例子:

>>> class Test(object):   #Must be new-style class!...  __slots__ = ['x', 'y']...>>> pt = Test()>>> dir(pt)['__class__', '__delattr__', '__doc__', '__getattribute__', '__hash__','__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__','__repr__', '__setattr__', '__slots__', '__str__', 'x', 'y']>>> pt.xTraceback (most recent call last):File "<stdin>", line 1, in <module>AttributeError: x>>> pt.x = 1>>> pt.x1>>> pt.z = 2Traceback (most recent call last):File "<stdin>", line 1, in <module>AttributeError: 'Test' object has no attribute 'z'>>> pt.__dict__Traceback (most recent call last):File "<stdin>", line 1, in <module>AttributeError: 'Test' object has no attribute '__dict__'>>> pt.__slots__['x', 'y']

因此,要实现__slots__,它只需要额外的一行(如果还没有,则使您的类成为新式类)。这样你就可以将这些类的内存占用减少5倍,如果有必要,你必须编写自定义泡菜代码。

最初的问题是关于一般用例,而不仅仅是关于内存。所以这里应该提到的是,当实例化大量对象时,你也会得到更好的性能-有趣的是,例如,当将大型文档解析为对象或从数据库解析时。

这是使用插槽和不使用插槽创建具有一百万个条目的对象树的比较。作为参考,还可以参考对树使用普通字典时的性能(OSX上的Py2.7.10):

********** RUN 1 **********1.96036410332 <class 'css_tree_select.element.Element'>3.02922606468 <class 'css_tree_select.element.ElementNoSlots'>2.90828204155 dict********** RUN 2 **********1.77050495148 <class 'css_tree_select.element.Element'>3.10655999184 <class 'css_tree_select.element.ElementNoSlots'>2.84120798111 dict********** RUN 3 **********1.84069895744 <class 'css_tree_select.element.Element'>3.21540498734 <class 'css_tree_select.element.ElementNoSlots'>2.59615707397 dict********** RUN 4 **********1.75041103363 <class 'css_tree_select.element.Element'>3.17366290092 <class 'css_tree_select.element.ElementNoSlots'>2.70941114426 dict

测试类(ident,appart from slot):

class Element(object):__slots__ = ['_typ', 'id', 'parent', 'childs']def __init__(self, typ, id, parent=None):self._typ = typself.id = idself.childs = []if parent:self.parent = parentparent.childs.append(self)
class ElementNoSlots(object): (same, w/o slots)

详细模式测试码:

na, nb, nc = 100, 100, 100for i in (1, 2, 3, 4):print '*' * 10, 'RUN', i, '*' * 10# tree with slot and no slot:for cls in Element, ElementNoSlots:t1 = time.time()root = cls('root', 'root')for i in xrange(na):ela = cls(typ='a', id=i, parent=root)for j in xrange(nb):elb = cls(typ='b', id=(i, j), parent=ela)for k in xrange(nc):elc = cls(typ='c', id=(i, j, k), parent=elb)to =  time.time() - t1print to, clsdel root
# ref: tree with dicts only:t1 = time.time()droot = {'childs': []}for i in xrange(na):ela =  {'typ': 'a', id: i, 'childs': []}droot['childs'].append(ela)for j in xrange(nb):elb =  {'typ': 'b', id: (i, j), 'childs': []}ela['childs'].append(elb)for k in xrange(nc):elc =  {'typ': 'c', id: (i, j, k), 'childs': []}elb['childs'].append(elc)td = time.time() - t1print td, 'dict'del droot

__slot__属性的一个非常简单的例子。

问题:没有__slots__

如果我的类中没有__slot__属性,我可以向我的对象添加新属性。

class Test:pass
obj1=Test()obj2=Test()
print(obj1.__dict__)  #--> {}obj1.x=12print(obj1.__dict__)  # --> {'x': 12}obj1.y=20print(obj1.__dict__)  # --> {'x': 12, 'y': 20}
obj2.x=99print(obj2.__dict__)  # --> {'x': 99}

如果你看上面的例子,你可以看到目标1Objectiv2有自己的xy属性,python也为每个对象(目标1Objectiv2)创建了一个dict属性。

假设我的类测试有数千个这样的对象?为每个对象创建一个附加属性dict将在我的代码中造成大量开销(内存、计算能力等)。

解决方案:__slots__

现在在下面的示例中,我的类测试包含__slots__属性。现在我不能向我的对象添加新属性(属性x除外),并且python不再创建dict属性。这消除了每个对象的开销,如果您有许多对象,这可能会变得很重要。

class Test:__slots__=("x")
obj1=Test()obj2=Test()obj1.x=12print(obj1.x)  # --> 12obj2.x=99print(obj2.x)  # --> 99
obj1.y=28print(obj1.y)  # --> AttributeError: 'Test' object has no attribute 'y'

__slots__的另一个有点模糊的用途是从ProxyTypes包中向对象代理添加属性,该包以前是PEAK项目的一部分。它的ObjectWrapper允许您代理另一个对象,但拦截与被代理对象的所有交互。它不是很常用(也不支持Python 3),但我们使用它来实现一个基于tornado的异步实现的线程安全阻塞包装器,该包装器通过ioloop反弹对被代理对象的所有访问,使用线程安全的concurrent.Future对象来同步和返回结果。

默认情况下,对代理对象的任何属性访问都将为您提供代理对象的结果。如果您需要在代理对象上添加属性,可以使用__slots__

from peak.util.proxies import ObjectWrapper
class Original(object):def __init__(self):self.name = 'The Original'
class ProxyOriginal(ObjectWrapper):
__slots__ = ['proxy_name']
def __init__(self, subject, proxy_name):# proxy_info attributed added directly to the# Original instance, not the ProxyOriginal instanceself.proxy_info = 'You are proxied by {}'.format(proxy_name)
# proxy_name added to ProxyOriginal instance, since it is# defined in __slots__self.proxy_name = proxy_name
super(ProxyOriginal, self).__init__(subject)
if __name__ == "__main__":original = Original()proxy = ProxyOriginal(original, 'Proxy Overlord')
# Both statements print "The Original"print "original.name: ", original.nameprint "proxy.name: ", proxy.name
# Both statements below print# "You are proxied by Proxy Overlord", since the ProxyOriginal# __init__ sets it to the original objectprint "original.proxy_info: ", original.proxy_infoprint "proxy.proxy_info: ", proxy.proxy_info
# prints "Proxy Overlord"print "proxy.proxy_name: ", proxy.proxy_name# Raises AttributeError since proxy_name is only set on# the proxy objectprint "original.proxy_name: ", proxy.proxy_name

从Python 3.9开始,可以使用dict通过__slots__向属性添加描述。None可以用于没有描述的属性,即使给出了描述,私有变量也不会出现。

class Person:
__slots__ = {"birthday":"A datetime.date object representing the person's birthday.","name":"The first and last name.","public_variable":None,"_private_variable":"Description",}

help(Person)"""Help on class Person in module __main__:
class Person(builtins.object)|  Data descriptors defined here:||  birthday|      A datetime.date object representing the person's birthday.||  name|      The first and last name.||  public_variable"""

除了其他答案之外,__slots__还通过将属性限制在预定义列表中来增加一点排版安全性。这一直是JavaScript的一个问题,它还允许您向现有对象添加新属性,无论您是否有意。

这是一个普通的未开槽对象,它什么都不做,但允许您添加属性:

class Unslotted:passtest = Unslotted()test.name = 'Fred'test.Name = 'Wilma'

由于Python区分大小写,因此拼写相同但大小写不同的两个属性是不同的。如果您怀疑其中一个是打字错误,那就倒霉了。

使用插槽,您可以限制:

class Slotted:__slots__ = ('name')passtest = Slotted()test.name = 'Fred'      #   OKtest.Name = 'Wilma'     #   Error

这一次,第二个属性(Name)被禁止,因为它不在__slots__集合中。

我建议最好在可能的情况下使用__slots__来保持对对象的更多控制。

除了在本文的其他答案中描述的无数优点——对于内存敏感的紧凑实例,比可变的__dict__实例更不容易出错,等等——我发现使用__slots__提供了更清晰的类声明,因为类的实例变量是显式公开的。

为了处理__slots__声明的继承问题,我使用这个元类:

import abc
class Slotted(abc.ABCMeta):    
""" A metaclass that ensures its classes, and all subclasses,will be slotted types."""    
def __new__(metacls, name, bases, attributes, **kwargs):""" Override for `abc.ABCMeta.__new__(…)` setting up aderived slotted class."""if '__slots__' not in attributes:attributes['__slots__'] = tuple()        
return super(Slotted, metacls).__new__(metacls, name, # type: ignorebases,attributes,**kwargs)

…如果在继承塔中声明为基类的元类,则确保从该基类派生的所有内容都将正确继承__slots__属性,即使中间类未能声明任何属性。如下所示:

# note no __slots__ declaration necessary with the metaclass:class Base(metaclass=Slotted):pass
# class is properly slotted, no __dict__:class Derived(Base):__slots__ = 'slot', 'another_slot'
# class is also properly slotted:class FurtherDerived(Derived):pass