【问题标题】:Is there a reason to prefer list or tuple for __slots__?是否有理由为 __slots__ 选择列表或元组?
【发布时间】:2014-01-21 18:08:51
【问题描述】:

您可以使用列表或元组(或者可能是任何可迭代的?)在新型 python 类中定义__slots__。创建实例后,该类型仍然存在。

鉴于元组总是比列表更高效并且是不可变的,你有什么理由不想为__slots__ 使用元组?

>>> class foo(object):
...   __slots__ = ('a',)
... 
>>> class foo2(object):
...   __slots__ = ['a']
... 
>>> foo().__slots__
('a',)
>>> foo2().__slots__
['a']

【问题讨论】:

  • 作为一个网站说明,我怀疑你和可能的人一样,认为__slots__ 以某种方式为你提供了更像是一个 C 结构的东西,它更快更紧凑,而且甚至可能是您首先使用它的原因。如果是这样:插槽使用描述符,因此访问foo.a 基本上是Foo.a.get(foo) 而不是通常的foo.__dict__['a'],这通常更慢而不是更快。优点是为每个实例保存一个 dict 对象,如果您有很多实例和很少的属性(如文档明确所说),而不是效率。
  • 仅供参考,在 Python 3.3 中有一个新的 dict 实现(参见 PEP412),这似乎使 __slots__ 变得多余。请参阅this question 了解更多信息。
  • @aquavitae:我认为那里的答案是错误的;对于大多数值得使用的用例,__slots__ 仍然不是多余的。

标签: python


【解决方案1】:

首先,元组并不比列表更有效;它们都支持与 C API 代码完全相同的快速迭代机制,并使用相同的代码进行索引和 Python 迭代。

更重要的是,__slots__ 机制实际上并不使用__slots__ 成员,除非在构造期间。 the documentation 可能没有清楚地解释这一点,但如果您足够仔细地阅读所有要点,信息就在那里。

真的,它必须是真的。否则,这将不起作用:

class Foo(object):
    __slots__ = (x for x in ['a', 'b', 'c'] if x != 'b')

……更糟糕的是,这会:

slots = ['a', 'b', 'c']
class Foo(object):
    __slots__ = slots
foo = Foo()
slots.append('d')
foo.d = 4

进一步证明:

>>> a = ['a', 'b']
>>> class Foo(object):
...     __slots__ = a
>>> del Foo.__slots__
>>> foo = Foo()
>>> foo.d = 3
AttributeError: 'Foo' object has no attribute 'd'
>>> foo.__dict__
AttributeError: 'Foo' object has no attribute '__dict__'
>>> foo.__slots__
AttributeError: 'Foo' object has no attribute '__slots__'

所以,Foo 中的 __slots__ 成员实际上仅用于文档和自省目的。这意味着没有性能问题或行为问题,只是风格问题。

【讨论】:

  • “首先,元组并不比列表更有效”——玩弄 timeit 证明了这一点
  • @ʞɔıu:你对他们做了什么,你在测试什么? ll=[randrange(100) for _ in range(10000)]; tt=tuple(ll) 后跟 %timeit ll[-100]%timeit tt[-100] 有 4 次获胜和 2 次失败的元组,但无论哪种方式都不会超过 3.5%。
  • timeit.Timer("for i in x: pass", "x = (1, 2, 3, 4, 5, 6)") 在我的系统上始终比 timeit.Timer("for i in x: pass", "x = [1, 2, 3, 4, 5, 6]") 快 20% 左右
  • @ʞɔıu: 使用你的确切代码,在我的 linux 机器上,在一台计算机上,我在 3.2.3 中快 2.8%,在 2.7.3 中慢 3.2%,在 2.7.6 中慢 0.6%;在我的 Mac 上,3.4b1 慢 1.9%,3.3.2 慢 1.3%,2.7.5 快 2.9%。 (所有 64 位 CPython。)但是,在相同的小序列上重复循环的性能无论如何在任何实际代码中都不太可能有意义。 (例如,在现实生活中,对于 99.99999% 的运行,您不会将整个序列保存在缓存中。)
  • @abarnert 我使用%timeit得到了与他的第一条评论相同的结果
【解决方案2】:

根据Python docs..

这个类变量可以被分配一个字符串、可迭代的或序列 实例使用的带有变量名的字符串。

因此,您可以使用任何可迭代对象来定义它。您使用哪一个取决于您,但就“更喜欢”哪个而言,我会使用一个列表。

首先,让我们看看如果性能不是问题,什么是首选,这意味着它会是所有 Python 代码中的same decision you would make between list and tuples。我会说一个列表,原因是因为元组被设计为具有语义结构:它在语义上应该意味着您将元素存储为第一项而不是第二项。例如,如果您将 (X,Y) 坐标元组 (X) 的第一个值存储为第二项,那么您就完全改变了结构的语义值。如果您重新排列__slots__ 列表中的属性名称,您并没有在语义上进行任何更改。因此,在这种情况下,您应该使用列表。

现在,关于性能。首先,这可能是过早的优化。我不知道列表和元组之间的性能差异,但我想反正没有。但即使假设存在,也只有在多次访问 __slots__ 变量时才会真正发挥作用。

我实际上并没有查看访问 __slots__ 时的代码,但我运行了以下测试..

print('Defining slotter..')
class Slotter(object):
    def __iter__(self):
        print('Looking for slots')
        yield 'A'
        yield 'B'
        yield 'C'

print('Defining Mine..')
class Mine(object):
    __slots__ = Slotter()

print('Creating first mine...')
m1 = Mine()
m1.A = 1
m1.B = 2

print('Creating second mine...')
m2 = Mine()
m2.A = 1
m2.C = 2

基本上,我使用了一个自定义类,这样我就可以准确地看到 slot 变量实际迭代的时间。当类被定义时,你会看到它只完成了一次。

Defining slotter..
Defining Mine..
Looking for slots
Creating first mine...
Creating second mine...

除非我错过了再次迭代 __slots__ 变量的情况,否则我认为性能差异在最坏的情况下可以被声明为可以忽略不计。

【讨论】:

  • 有没有办法在不再次访问__slots__ 的情况下获取具有插槽的对象的属性列表?
  • re:“元组被设计为具有语义结构:它应该在语义上意味着您将元素存储为第一项而不是第二项”——我认为这是您想要使用元组而不是完整集的原因。另一个原因是可变性,__slots__ 绝对没有理由可变。
  • 在我看来完全是假的。 listtuple 都是序列,两者的顺序可能很重要。如果您真的想要一个在语义上暗示顺序无关紧要的容器,我想您可以使用set,因为__slots__ 不需要重复。即使您接受同质与异质的区别(IMO 是可变大小与固定大小的副作用,但我们不要去那里),这并不意味着 list 暗示 无序 数据。
  • 正如我的回答所解释的,无论您传入什么可迭代对象,您的实际插槽都不会是可变的。如果使用 tuple 会误导您认为存在不存在的差异,这就是避免它的充分理由……@SteveJessop:我实际上已经使用了几次set,我认为这样读起来很不错,但我觉得不足以支持它。
  • @abarnert:是的,如果不是设置文字,我认为set 甚至不会成为竞争者:-)
猜你喜欢
  • 1970-01-01
  • 2010-12-22
  • 2017-09-05
  • 2011-10-01
  • 2018-07-17
  • 1970-01-01
  • 1970-01-01
  • 2021-08-12
  • 2020-03-05
相关资源
最近更新 更多