使用插槽?

2024-03-28 22:02:26 发布

您现在位置:Python中文网/ 问答频道 /正文

Python中^{}的目的是什么——特别是当我想使用它时,或者不想使用它时?


Tags: 目的
3条回答

如果要实例化同一类的许多(成百上千)对象,则需要使用__slots____slots__仅作为内存优化工具存在。

强烈建议使用__slots__来约束属性创建。

使用__slots__的pickle对象不能使用默认(最旧的)pickle协议;需要指定更高版本。

python的一些其他内省特性也可能受到不利影响。

引用Jacob Hallen

The proper use of __slots__ is to save space in objects. Instead of having a dynamic dict that allows adding attributes to objects at anytime, there is a static structure which does not allow additions after creation. [This use of __slots__ eliminates the overhead of one dict for every object.] While this is sometimes a useful optimization, it would be completely unnecessary if the Python interpreter was dynamic enough so that it would only require the dict when there actually were additions to the object.

Unfortunately there is a side effect to slots. They change the behavior of the objects that have slots in a way that can be abused by control freaks and static typing weenies. This is bad, because the control freaks should be abusing the metaclasses and the static typing weenies should be abusing decorators, since in Python, there should be only one obvious way of doing something.

Making CPython smart enough to handle saving space without __slots__ is a major undertaking, which is probably why it is not on the list of changes for P3k (yet).

In Python, what is the purpose of __slots__ and what are the cases one should avoid this?

TLDR编号:

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

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

节省的空间来自

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

快速警告

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

class Base:
    __slots__ = 'foo', 'bar'

class Right(Base):
    __slots__ = 'baz', 

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

Python不反对当你弄错了(它可能应该),问题可能不会以其他方式显现,但是你的对象将占用比他们应该占用更多的空间。

>>> from sys import getsizeof
>>> getsizeof(Right()), getsizeof(Wrong())
(64, 80)

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

为了适应这种限制,请遵循最佳实践:去掉所有父类的抽象(除了一个或所有父类之外),这些父类的具体类和新的具体类将共同继承自这些抽象(就像标准库中的抽象基类一样)。

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

要求:

  • 若要使在__slots__中命名的属性实际存储在插槽中而不是__dict__,则类必须从object继承。

  • 为了防止创建__dict__,必须从object继承,继承中的所有类都必须声明__slots__,并且任何类都不能有'__dict__'项。

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

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

Python的创建者Guido van Rossum,states实际上是他为了更快的属性访问而创建的__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.foo
        del obj.foo
    return 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.2846834529991611
1.2873325658284342

在Windows上的Python2中,我已经测量到它大约快了15%。

为什么使用__slots__:节省内存

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

My own contribution to the documentation clearly states the reasons behind this

The space saved over using __dict__ can be significant.

SQLAlchemy attributes__slots__节省了大量内存。

为了验证这一点,在Ubuntu Linux上使用Python 2.7的Anaconda发行版,在声明了guppy.hpy(又名heapy)和sys.getsizeof的情况下,没有声明__slots__的类实例的大小是64字节。这并不包括__dict__。再次感谢Python的懒惰计算,__dict__在被引用之前显然不会被调用,但是没有数据的类通常是无用的。当被调用存在时,__dict__属性至少额外280字节。

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

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

       Python 2.7             Python 3.6
attrs  __slots__  __dict__*   __slots__  __dict__* | *(no slots defined)
none   16         56 + 272†   16         56 + 112† | †if __dict__ referenced
one    48         56 + 272    48         56 + 112
two    56         56 + 272    56         56 + 112
six    88         56 + 1040   88         56 + 152
11     128        56 + 1040   128        56 + 240
22     216        56 + 3344   216        56 + 408     
43     384        56 + 3344   384        56 + 752

因此,尽管Python 3中有更小的dict,但是我们可以看到实例的__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

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): pass
ns = 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 bases
    multiple 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 namedtuple
class 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__创建,但不能对tuple的子类型使用非空__slots__

最大的警告:多重继承

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

class Foo(object): 
    __slots__ = 'foo', 'bar'
class Bar(object):
    __slots__ = 'foo', 'bar' # alas, would work if empty, i.e. ()

>>> class Baz(Foo, Bar): pass
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Error when calling the metaclass bases
    multiple bases have instance lay-out conflict

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

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

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

在这里走投无路:如果您正在编写mixins或使用abstract base classes(不打算被实例化),在这些父类中使用空的__slots__似乎是子类灵活性方面的最佳方法。

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

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

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

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

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

class AbstractBaseC:
    __slots__ = ()
    @property
    def c(self):
        print('getting c!')
        return self._c
    @c.setter
    def 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 = c
setting c!
>>> c.c
getting 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__documentation (the 3.7 dev docs are the most current)的其余部分中梳理出进一步的注意事项,我最近对这些内容做出了重要贡献。

对其他答案的评论

目前最热门的答案引用了过时的信息,在一些重要的方面是相当不得体的。

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

我引用:

"You would want to use __slots__ if you are going to instantiate a lot (hundreds, thousands) of objects of the same class."

例如,来自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>

在Python2.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日)接受答案的评论

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

The proper use of __slots__ is to save space in objects. Instead of having a dynamic dict that allows adding attributes to objects at anytime, there is a static structure which does not allow additions after creation. This saves the overhead of one dict for every object that uses slots

下半场是一厢情愿的想法,而且离题了:

While this is sometimes a useful optimization, it would be completely unnecessary if the Python interpreter was dynamic enough so that it would only require the dict when there actually were additions to the object.

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

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

They change the behavior of the objects that have slots in a way that can be abused by control freaks and static typing weenies.

然后讨论用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__.Foo
     1     169   0 16281480  16  80281480  80 list
     2 1000000  49 16000000  16  96281480  97 __main__.Bar
     3   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__.Foo
     1 1000000  33  64000000  17 344000000  91 __main__.Foo
     2     169   0  16281480   4 360281480  95 list
     3 1000000  33  16000000   4 376281480  99 __main__.Bar
     4   12284   0    987472   0 377268952  99 str
...

这与Python的历史是一致的,从Unifying types and classes in Python 2.2

If you subclass a built-in type, extra space is automatically added to the instances to accomodate __dict__ and __weakrefs__. (The __dict__ is not initialized until you use it though, so you shouldn't worry about the space occupied by an empty dictionary for each instance you create.) If you don't need this extra space, you can add the phrase "__slots__ = []" to your class.

相关问题 更多 >