为什么要在__slots__中添加__dict__属性?

2019-01-18 15:44:46 +08:00
 whoami9894

读 Werkzeug 源码时看到:

class LocalProxy(object):
	__slots__ = ('__local', '__dict__', '__name__', '__wrapped__')
    
    # ...

这样写的目的是什么

sf 上相关的提问没有结论: https://stackoverflow.com/questions/7585284/python-whats-the-point-of-adding-dict-to-slots

2652 次点击
所在节点    Python
13 条回复
yangsi
2019-01-18 17:12:28 +08:00
为了支持动态创建属性。
brucedone
2019-01-18 17:17:22 +08:00
j0hnj
2019-01-18 17:27:14 +08:00
emmm,表示跟楼主有一样的疑惑。按道理来说,使用 `__slots__` 就是为了避免创建 `__dict__` 这个字典,然而又把 `__dict__` 加到 `__slots__` 中,实在是有点讲不通。
yangsi
2019-01-18 17:29:28 +08:00
添加__dict__之后使对象有了动态添加属性的能力,但是定义在__solt
yangsi
2019-01-18 17:30:26 +08:00
定义在 slots 里面的属性还是不保存在 dict 里面。
whoami9894
2019-01-18 23:28:08 +08:00
@yangsi
@brucedone

你们搞懂我在问什么了吗。。。看#3
whoami9894
2019-01-19 00:03:04 +08:00
我查看了文档,提到`__slots__`不仅会去掉实例的`__dict__`属性,还会去掉`__weakref__`属性。

> This class variable can be assigned a string, iterable, or sequence of strings with variable names used by instances. __slots__ reserves space for the declared variables and prevents the automatic creation of __dict__ and __weakref__ for each instance.

所以这里的目的可能是为了使`LocalProxy`类不可被弱引用(?存疑)
aijam
2019-01-19 10:31:15 +08:00
TLDR:
使用__slots__是为了节约内存使用,但是带来的两个副作用:
1. 没了__dict__,无法动态加属性。
2. 没了__weakref__,无法使用弱引用。
为了克服这两个副作用需要把它们重新加回去。

=================================================
1. 普通的 class 会在 instance 初始化的时候把 attribute 放到__dict__里,也就是说内部维护了一个多余的 dict。
>>> class A():
... def __init__(self):
... self.x = 1
... self.y = 2
...
>>> a = A()
>>> a.__dict__
{'x': 1, 'y': 2}

2. 为了避免在__dict__里浪费内存,有了__slots__。
>>> class B():
... __slots__ = ('x', 'y')
... def __init__(self):
... self.x = 1
... self.y = 2
...
>>> b = B()
>>> b.__dict__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'B' object has no attribute '__dict__'
可以看出__dict__消失了。

3. __dict__的存在目的是为了能在 instance 里动态加入新的属性,新的属性会加到__dict__里。
>>> a.z = 3
>>> a.__dict__
{'x': 1, 'y': 2, 'z': 3}

但用了__slots__后就无法动态加属性了。
>>> b.z = 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'B' object has no attribute 'z'

4. 为了依然能动态加属性,我们在__slots__里重新加入__dict__。
>>> class C():
... __slots__ = ('x', 'y', '__dict__')
... def __init__(self):
... self.x = 1
... self.y = 2
...
>>> c = C()
>>> c.__dict__
{}
我们注意到,初始化时__dict__初始是空的,依然比 a 要节约内存。
这时候动态加属性也没问题了。
>>> c.z = 3
>>> c.__dict__
{'z': 3}

5. 具体验证下__slots__到底做了什么。
>>> set(dir(b)) - set(dir(a))
{'__slots__'}
>>> set(dir(a)) - set(dir(b))
{'__dict__', '__weakref__'}
可以看出 b 加了__slots__后,相较 a 少了__dict__以及__weakref__。
同理,为了使用弱引用,需要把__weakref__加回去。

但有一点我还存有疑问:当初设计__slots__时为什么要去掉__weakref__?
aijam
2019-01-19 10:42:18 +08:00
当然这里有个不严谨的地方:例子里{'x': 1, 'y': 2}并不一定会比空的 dict 占用更多内存,这和初始时 attribute 的个数,dict 底层实现的初始大小 /load factor 等有关。
zh826256645
2019-01-19 10:55:19 +08:00
class LocalProxy(object):__slots__ = ('__local', '__dict__', '__name__', '__wrapped__')
In [3] used 0.0312 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 33.28 MiB

In [4]: lp = LocalProxy()
In [4] used 0.0391 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 33.32 MiB

In [5]: lp
Out[5]: <__main__.LocalProxy at 0x10324e5f0>
In [5] used 0.0117 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 33.33 MiB

In [6]: class LocalProxy(object):
...: pass
...:
In [6] used 0.4688 MiB RAM in 0.11s, peaked 0.00 MiB above current, total RAM usage 33.80 MiB

In [7]: lp = LocalProxy()
In [7] used 0.0508 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 33.85 MiB

In [8]: lp
Out[8]: <__main__.LocalProxy at 0x103349110>
In [8] used 0.0039 MiB RAM in 0.11s, peaked 0.00 MiB above current, total RAM usage 33.86 MiB

--------------------------------------- 看看楼上老哥的例子 -------------------------------------------------

In [9]: class C(object):
...: __slots__ = ('x', 'y', '__dict__')
...: def __init__(self):
...: self.x = 1
...: self.y = 2
...:
In [9] used 0.2305 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 34.09 MiB

In [10]: c = C()
In [10] used 0.0000 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 34.09 MiB

In [11]: c
Out[11]: <__main__.C at 0x103147c68>
In [11] used 0.0039 MiB RAM in 0.11s, peaked 0.00 MiB above current, total RAM usage 34.09 MiB

In [12]: class C(object):
...: def __init__(self):
...: self.x = 1
...: self.y = 2
...:
In [12] used 0.0430 MiB RAM in 0.11s, peaked 0.00 MiB above current, total RAM usage 34.13 MiB

In [13]: c = C()
In [13] used 0.0000 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 34.13 MiB

In [14]: c
Out[14]: <__main__.C at 0x10335d210>
In [14] used 0.0000 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 34.13 MiB


事实证明确实是为了尽可能的省内存,想省内存,但是又不想丢弃 __dict__,__weakref__ 这两个功能
只能说细真的细
whoami9894
2019-01-19 15:38:58 +08:00
@aijam
@j0hnj
@zh826256645

我明白了,是为了能够转发 被代理 obj 的__dict__属性,我看了别处对`LocalProxy`的使用没有动态新增实例属性,而`LocalProxy`的实现里唯一的属性(除开`__slots__`里的属性)是这个:

```python
@property
def __dict__(self):
try:
return self._get_current_object().__dict__
except RuntimeError:
raise AttributeError('__dict__')
```
zh826256645
2019-01-19 16:35:33 +08:00
LocalProxy 确实有点东西,是再看 flask 的源码吗?
whoami9894
2019-01-19 16:50:01 +08:00
@zh826256645
是的,想看看 Flask 的 ctx 怎么实现的

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/528326

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX