python - __slots__ 的使用?

标签 python oop python-internals slots

__slots__ 的目的是什么?在 Python 中——尤其是关于我什么时候想使用它,什么时候不想使用它?

最佳答案

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


域名注册地址:
特殊属性 __slots__ 允许您明确说明您希望对象实例具有哪些实例属性,并具有预期结果:
  • 更快 属性访问。
  • 节省空间在内存中。

  • 节省的空间来自
  • 在槽中存储值引用而不是 __dict__ .
  • 拒绝 __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 不会反对(它可能应该),问题可能不会以其他方式出现,但是您的对象将占用更多的空间。 python 3.8:
    >>> from sys import getsizeof
    >>> getsizeof(Right()), getsizeof(Wrong())
    (56, 72)
    
    这是因为 Base 的插槽描述符有一个与 Wrong 的插槽分开的插槽。这通常不应该出现,但它可以:
    >>> 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,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 上的 Python 2 中,我测得它快了大约 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 (又名肥大)和 sys.getsizeof , 没有 __slots__ 的类实例的大小声明,没有别的,是 64 字节。这不包括 __dict__ .再次感谢 Python 的懒惰评估,__dict__显然在被引用之前不会被调用,但是没有数据的类通常是无用的。当被调用时,__dict__属性至少为 280 字节。
    相比之下,带有 __slots__ 的类实例声明为 () (无数据)只有 16 个字节,插槽中有一个项目总共 56 个字节,两个有 64 个字节。
    对于 64 位 Python,我说明了 Python 2.7 和 3.6 中以字节为单位的内存消耗,用于 __slots____dict__ (未定义插槽)对于 dict 在 3.6 中增长的每个点(0、1 和 2 属性除外):
           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 中的字典更小,但我们看到 __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 .一切都是子类 object在 Python 3 中,但在 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): 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__来自 parent ,或者如果你可以控制 parent ,给他们空位,或者重构抽象:
    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 内置使不可变实例非常轻量级(本质上是元组的大小),但要获得好处,如果您对它们进行子类化,则需要自己做:
    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'
    
    您可以允许 __dict__离开创造__slots__ = () ,但不能使用非空 __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__在 parent 中似乎提供了最大的灵活性,允许 child 选择阻止或允许 (通过添加 '__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
    
    现在如果两个 base 都有非空槽,我们就不能做下面的事情。 (事实上​​,如果我们愿意,我们可以给 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) ,我最近做出了重大贡献。
    对其他答案的批评
    当前 HitTest 门的答案引用了过时的信息,并且在某些重要方面非常不准确。
    不要“在实例化大量对象时只使用 __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__在创建接口(interface)或 mixin 时有助于提高可重用性。
    确实,许多 Python 用户并不是为了可重用性而编写代码,但是当您这样做时,选择拒绝不必要的空间使用是很有值(value)的。__slots__不会破坏酸洗
    酸洗开槽对象时,您可能会发现它提示带有误导性 TypeError :
    >>> pickle.loads(pickle.dumps(f))
    TypeError: a class that defines __slots__ without defining __getstate__ cannot be pickled
    
    这实际上是不正确的。此消息来自最旧的协议(protocol),这是默认的。您可以使用 -1 选择最新的协议(protocol)。争论。在 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>
    
    在 Python 3.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.

    关于python - __slots__ 的使用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/472000/

    相关文章:

    python - 以编程方式加载/卸载插件并更改 Gedit 3 中的设置

    python - Django 上下文处理器问题

    c++ - 制作 GUI 时清理 OOP 设计

    c# - 在类及其成员之间共享变量

    java - 如何将输出语句转换为字符串类型?

    python - 捕获在 docker 容器内运行的 python 脚本的输出

    python - 在 2 个列表中查找最接近的日期时间

    python 字典是指向...什么的指针的 HashMap ?

    python - 关闭 __class__

    Python 评估 : is it still dangerous if I disable builtins and attribute access?