Skip to content

Commit 4de6a21

Browse files
committed
8.25小节完成,第八章完成。
1 parent 1a39696 commit 4de6a21

File tree

2 files changed

+318
-3
lines changed

2 files changed

+318
-3
lines changed

cookbook/c08/p25_cached_objects.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env python
2+
# -*- encoding: utf-8 -*-
3+
"""
4+
Topic: 创建缓存实例
5+
Desc :
6+
"""
7+
8+
import logging
9+
10+
a = logging.getLogger('foo')
11+
b = logging.getLogger('bar')
12+
print(a is b)
13+
c = logging.getLogger('foo')
14+
print(a is c)
15+
16+
# The class in question
17+
class Spam:
18+
def __init__(self, name):
19+
self.name = name
20+
21+
# Caching support
22+
import weakref
23+
24+
_spam_cache = weakref.WeakValueDictionary()
25+
26+
27+
def get_spam(name):
28+
if name not in _spam_cache:
29+
s = Spam(name)
30+
_spam_cache[name] = s
31+
else:
32+
s = _spam_cache[name]
33+
return s
34+
35+
36+
# Note: This code doesn't quite work
37+
class Spam1:
38+
_spam_cache = weakref.WeakValueDictionary()
39+
40+
def __new__(cls, name):
41+
print('Spam1__new__')
42+
if name in cls._spam_cache:
43+
return cls._spam_cache[name]
44+
else:
45+
self = super().__new__(cls)
46+
cls._spam_cache[name] = self
47+
return self
48+
49+
def __init__(self, name):
50+
print('Initializing Spam')
51+
self.name = name
52+
53+
54+
s = Spam1('Dave')
55+
t = Spam1('Dave')
56+
print(s is t)
57+
58+
59+
class CachedSpamManager:
60+
def __init__(self):
61+
self._cache = weakref.WeakValueDictionary()
62+
63+
def get_spam(self, name):
64+
if name not in self._cache:
65+
s = Spam(name)
66+
self._cache[name] = s
67+
else:
68+
s = self._cache[name]
69+
return s
70+
71+
def clear(self):
72+
self._cache.clear()
73+
74+
75+
class Spam2:
76+
manager = CachedSpamManager()
77+
78+
def __init__(self, name):
79+
self.name = name
80+
81+
def get_spam(name):
82+
return Spam2.manager.get_spam(name)
83+
84+
85+
# ------------------------最后的修正方案------------------------
86+
class CachedSpamManager2:
87+
def __init__(self):
88+
self._cache = weakref.WeakValueDictionary()
89+
90+
def get_spam(self, name):
91+
if name not in self._cache:
92+
temp = Spam3._new(name) # Modified creation
93+
self._cache[name] = temp
94+
else:
95+
temp = self._cache[name]
96+
return temp
97+
98+
def clear(self):
99+
self._cache.clear()
100+
101+
102+
class Spam3:
103+
def __init__(self, *args, **kwargs):
104+
raise RuntimeError("Can't instantiate directly")
105+
106+
# Alternate constructor
107+
@classmethod
108+
def _new(cls, name):
109+
self = cls.__new__(cls)
110+
self.name = name
111+
return self
112+
113+
print('------------------------------')
114+
cachedSpamManager = CachedSpamManager2()
115+
s = cachedSpamManager.get_spam('Dave')
116+
t = cachedSpamManager.get_spam('Dave')
117+
print(s is t)

source/c08/p25_creating_cached_instances.rst

Lines changed: 201 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,212 @@
55
----------
66
问题
77
----------
8-
todo...
8+
在创建一个类的对象时,如果之前使用同样参数创建过这个对象, 你想返回它的缓存引用。
9+
10+
|
911
1012
----------
1113
解决方案
1214
----------
13-
todo...
15+
这种通常是因为你希望相同参数创建的对象时单例的。
16+
在很多库中都有实际的例子,比如 ``logging`` 模块,使用相同的名称创建的 ``logger`` 实例永远只有一个。例如:
17+
18+
.. code-block:: python
19+
20+
>>> import logging
21+
>>> a = logging.getLogger('foo')
22+
>>> b = logging.getLogger('bar')
23+
>>> a is b
24+
False
25+
>>> c = logging.getLogger('foo')
26+
>>> a is c
27+
True
28+
>>>
29+
30+
为了达到这样的效果,你需要使用一个和类本身分开的工厂函数,例如:
31+
32+
.. code-block:: python
33+
34+
# The class in question
35+
class Spam:
36+
def __init__(self, name):
37+
self.name = name
38+
39+
# Caching support
40+
import weakref
41+
_spam_cache = weakref.WeakValueDictionary()
42+
def get_spam(name):
43+
if name not in _spam_cache:
44+
s = Spam(name)
45+
_spam_cache[name] = s
46+
else:
47+
s = _spam_cache[name]
48+
return s
49+
50+
然后做一个测试,你会发现跟之前那个日志对象的创建行为是一致的:
51+
52+
.. code-block:: python
53+
54+
>>> a = get_spam('foo')
55+
>>> b = get_spam('bar')
56+
>>> a is b
57+
False
58+
>>> c = get_spam('foo')
59+
>>> a is c
60+
True
61+
>>>
62+
63+
|
1464
1565
----------
1666
讨论
1767
----------
18-
todo...
68+
编写一个工厂函数来修改普通的实例创建行为通常是一个比较简单的方法。
69+
但是我们还能否找到更优雅的解决方案呢?
70+
71+
例如,你可能会考虑重新定义类的 ``__new__()`` 方法,就像下面这样:
72+
73+
.. code-block:: python
74+
75+
# Note: This code doesn't quite work
76+
import weakref
77+
78+
class Spam:
79+
_spam_cache = weakref.WeakValueDictionary()
80+
def __new__(cls, name):
81+
if name in cls._spam_cache:
82+
return cls._spam_cache[name]
83+
else:
84+
self = super().__new__(cls)
85+
cls._spam_cache[name] = self
86+
return self
87+
def __init__(self, name):
88+
print('Initializing Spam')
89+
self.name = name
90+
91+
初看起来好像可以达到预期效果,但是问题是 ``__init__()`` 每次都会被调用,不管这个实例是否被缓存了。例如:
92+
93+
.. code-block:: python
94+
95+
>>> s = Spam('Dave')
96+
Initializing Spam
97+
>>> t = Spam('Dave')
98+
Initializing Spam
99+
>>> s is t
100+
True
101+
>>>
102+
103+
这个或许不是你想要的效果,因此这种方法并不可取。
104+
105+
上面我们使用到了弱引用计数,对于垃圾回收来讲是很有帮助的,关于这个我们在8.23小节已经讲过了。
106+
当我们保持实例缓存时,你可能只想在程序中使用到它们时才保存。
107+
一个 ``WeakValueDictionary`` 实例只会保存那些在其它地方还在被使用的实例。
108+
否则的话,只要实例不再被使用了,它就从字典中被移除了。观察下下面的测试结果:
109+
110+
.. code-block:: python
111+
112+
>>> a = get_spam('foo')
113+
>>> b = get_spam('bar')
114+
>>> c = get_spam('foo')
115+
>>> list(_spam_cache)
116+
['foo', 'bar']
117+
>>> del a
118+
>>> del c
119+
>>> list(_spam_cache)
120+
['bar']
121+
>>> del b
122+
>>> list(_spam_cache)
123+
[]
124+
>>>
125+
126+
对于大部分程序而已,这里代码已经够用了。不过还是有一些更高级的实现值得了解下。
127+
128+
首先是这里使用到了一个全局变量,并且工厂函数跟类放在一块。我们可以通过将缓存代码放到一个单独的缓存管理器中:
129+
130+
.. code-block:: python
131+
132+
import weakref
133+
134+
class CachedSpamManager:
135+
def __init__(self):
136+
self._cache = weakref.WeakValueDictionary()
137+
138+
def get_spam(self, name):
139+
if name not in self._cache:
140+
s = Spam(name)
141+
self._cache[name] = s
142+
else:
143+
s = self._cache[name]
144+
return s
145+
146+
def clear(self):
147+
self._cache.clear()
148+
149+
class Spam:
150+
manager = CachedSpamManager()
151+
def __init__(self, name):
152+
self.name = name
153+
154+
def get_spam(name):
155+
return Spam.manager.get_spam(name)
156+
157+
这样的话代码更清晰,并且也更灵活,我们可以增加更多的缓存管理机制,只需要替代manager即可。
158+
159+
还有一点就是,我们暴露了类的实例化给用户,用户很容易去直接实例化这个类,而不是使用工厂方法,如:
160+
161+
.. code-block:: python
162+
163+
>>> a = Spam('foo')
164+
>>> b = Spam('foo')
165+
>>> a is b
166+
False
167+
>>>
168+
169+
有几种方式可以防止用户这样做,第一个是将类的名字修改为以下划线(_)开头,提示用户别直接调用它。
170+
第二种就是让这个类的 ``__init__()`` 方法抛出一个异常,让它不能被初始化:
171+
172+
.. code-block:: python
173+
174+
class Spam:
175+
def __init__(self, *args, **kwargs):
176+
raise RuntimeError("Can't instantiate directly")
177+
178+
# Alternate constructor
179+
@classmethod
180+
def _new(cls, name):
181+
self = cls.__new__(cls)
182+
self.name = name
183+
184+
然后修改缓存管理器代码,使用 ``Spam._new()`` 来创建实例,而不是直接调用 ``Spam()`` 构造函数:
185+
186+
.. code-block:: python
187+
188+
# ------------------------最后的修正方案------------------------
189+
class CachedSpamManager2:
190+
def __init__(self):
191+
self._cache = weakref.WeakValueDictionary()
192+
193+
def get_spam(self, name):
194+
if name not in self._cache:
195+
temp = Spam3._new(name) # Modified creation
196+
self._cache[name] = temp
197+
else:
198+
temp = self._cache[name]
199+
return temp
200+
201+
def clear(self):
202+
self._cache.clear()
203+
204+
class Spam3:
205+
def __init__(self, *args, **kwargs):
206+
raise RuntimeError("Can't instantiate directly")
207+
208+
# Alternate constructor
209+
@classmethod
210+
def _new(cls, name):
211+
self = cls.__new__(cls)
212+
self.name = name
213+
return self
214+
215+
最后这样的方案就已经足够好了。
216+
缓存和其他构造模式还可以使用9.13小节中的元类实现的更优雅一点(使用了更高级的技术)。

0 commit comments

Comments
 (0)