8.25 創(chuàng)建緩存實(shí)例

2018-02-24 15:26 更新

問(wèn)題

在創(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ù))。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)