W3Cschool
恭喜您成為首批注冊(cè)用戶(hù)
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
在創(chuàng)建一個(gè)類(lèi)的對(duì)象時(shí),如果之前使用同樣參數(shù)創(chuàng)建過(guò)這個(gè)對(duì)象, 你想返回它的緩存引用。
這種通常是因?yàn)槟阆M嗤瑓?shù)創(chuàng)建的對(duì)象時(shí)單例的。在很多庫(kù)中都有實(shí)際的例子,比如 logging
模塊,使用相同的名稱(chēng)創(chuàng)建的 logger
實(shí)例永遠(yuǎn)只有一個(gè)。例如:
>>> import logging
>>> a = logging.getLogger('foo')
>>> b = logging.getLogger('bar')
>>> a is b
False
>>> c = logging.getLogger('foo')
>>> a is c
True
>>>
為了達(dá)到這樣的效果,你需要使用一個(gè)和類(lèi)本身分開(kāi)的工廠函數(shù),例如:
# The class in question
class Spam:
def __init__(self, name):
self.name = name
# Caching support
import weakref
_spam_cache = weakref.WeakValueDictionary()
def get_spam(name):
if name not in _spam_cache:
s = Spam(name)
_spam_cache[name] = s
else:
s = _spam_cache[name]
return s
然后做一個(gè)測(cè)試,你會(huì)發(fā)現(xiàn)跟之前那個(gè)日志對(duì)象的創(chuàng)建行為是一致的:
>>> a = get_spam('foo')
>>> b = get_spam('bar')
>>> a is b
False
>>> c = get_spam('foo')
>>> a is c
True
>>>
編寫(xiě)一個(gè)工廠函數(shù)來(lái)修改普通的實(shí)例創(chuàng)建行為通常是一個(gè)比較簡(jiǎn)單的方法。但是我們還能否找到更優(yōu)雅的解決方案呢?
例如,你可能會(huì)考慮重新定義類(lèi)的 __new__()
方法,就像下面這樣:
# Note: This code doesn't quite work
import weakref
class Spam:
_spam_cache = weakref.WeakValueDictionary()
def __new__(cls, name):
if name in cls._spam_cache:
return cls._spam_cache[name]
else:
self = super().__new__(cls)
cls._spam_cache[name] = self
return self
def __init__(self, name):
print('Initializing Spam')
self.name = name
初看起來(lái)好像可以達(dá)到預(yù)期效果,但是問(wèn)題是 __init__()
每次都會(huì)被調(diào)用,不管這個(gè)實(shí)例是否被緩存了。例如:
>>> s = Spam('Dave')
Initializing Spam
>>> t = Spam('Dave')
Initializing Spam
>>> s is t
True
>>>
這個(gè)或許不是你想要的效果,因此這種方法并不可取。
上面我們使用到了弱引用計(jì)數(shù),對(duì)于垃圾回收來(lái)講是很有幫助的,關(guān)于這個(gè)我們?cè)?.23小節(jié)已經(jīng)講過(guò)了。當(dāng)我們保持實(shí)例緩存時(shí),你可能只想在程序中使用到它們時(shí)才保存。一個(gè) WeakValueDictionary
實(shí)例只會(huì)保存那些在其它地方還在被使用的實(shí)例。否則的話(huà),只要實(shí)例不再被使用了,它就從字典中被移除了。觀察下下面的測(cè)試結(jié)果:
>>> a = get_spam('foo')
>>> b = get_spam('bar')
>>> c = get_spam('foo')
>>> list(_spam_cache)
['foo', 'bar']
>>> del a
>>> del c
>>> list(_spam_cache)
['bar']
>>> del b
>>> list(_spam_cache)
[]
>>>
對(duì)于大部分程序而已,這里代碼已經(jīng)夠用了。不過(guò)還是有一些更高級(jí)的實(shí)現(xiàn)值得了解下。
首先是這里使用到了一個(gè)全局變量,并且工廠函數(shù)跟類(lèi)放在一塊。我們可以通過(guò)將緩存代碼放到一個(gè)單獨(dú)的緩存管理器中:
import weakref
class CachedSpamManager:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get_spam(self, name):
if name not in self._cache:
s = Spam(name)
self._cache[name] = s
else:
s = self._cache[name]
return s
def clear(self):
self._cache.clear()
class Spam:
manager = CachedSpamManager()
def __init__(self, name):
self.name = name
def get_spam(name):
return Spam.manager.get_spam(name)
這樣的話(huà)代碼更清晰,并且也更靈活,我們可以增加更多的緩存管理機(jī)制,只需要替代manager即可。
還有一點(diǎn)就是,我們暴露了類(lèi)的實(shí)例化給用戶(hù),用戶(hù)很容易去直接實(shí)例化這個(gè)類(lèi),而不是使用工廠方法,如:
>>> a = Spam('foo')
>>> b = Spam('foo')
>>> a is b
False
>>>
有幾種方式可以防止用戶(hù)這樣做,第一個(gè)是將類(lèi)的名字修改為以下劃線(_)開(kāi)頭,提示用戶(hù)別直接調(diào)用它。第二種就是讓這個(gè)類(lèi)的 __init__()
方法拋出一個(gè)異常,讓它不能被初始化:
class Spam:
def __init__(self, *args, **kwargs):
raise RuntimeError("Can't instantiate directly")
# Alternate constructor
@classmethod
def _new(cls, name):
self = cls.__new__(cls)
self.name = name
然后修改緩存管理器代碼,使用 Spam._new()
來(lái)創(chuàng)建實(shí)例,而不是直接調(diào)用 Spam()
構(gòu)造函數(shù):
# ------------------------最后的修正方案------------------------
class CachedSpamManager2:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get_spam(self, name):
if name not in self._cache:
temp = Spam3._new(name) # Modified creation
self._cache[name] = temp
else:
temp = self._cache[name]
return temp
def clear(self):
self._cache.clear()
class Spam3:
def __init__(self, *args, **kwargs):
raise RuntimeError("Can't instantiate directly")
# Alternate constructor
@classmethod
def _new(cls, name):
self = cls.__new__(cls)
self.name = name
return self
最后這樣的方案就已經(jīng)足夠好了。緩存和其他構(gòu)造模式還可以使用9.13小節(jié)中的元類(lèi)實(shí)現(xiàn)的更優(yōu)雅一點(diǎn)(使用了更高級(jí)的技術(shù))。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話(huà):173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: