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

2018-02-24 15:26 更新

問題

在創(chuàng)建一個類的對象時,如果之前使用同樣參數(shù)創(chuàng)建過這個對象, 你想返回它的緩存引用。

解決方案

這種通常是因為你希望相同參數(shù)創(chuàng)建的對象時單例的。在很多庫中都有實(shí)際的例子,比如 logging 模塊,使用相同的名稱創(chuàng)建的 logger 實(shí)例永遠(yuǎn)只有一個。例如:

>>> import logging
>>> a = logging.getLogger('foo')
>>> b = logging.getLogger('bar')
>>> a is b
False
>>> c = logging.getLogger('foo')
>>> a is c
True
>>>

為了達(dá)到這樣的效果,你需要使用一個和類本身分開的工廠函數(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

然后做一個測試,你會發(fā)現(xiàn)跟之前那個日志對象的創(chuàng)建行為是一致的:

>>> a = get_spam('foo')
>>> b = get_spam('bar')
>>> a is b
False
>>> c = get_spam('foo')
>>> a is c
True
>>>

討論

編寫一個工廠函數(shù)來修改普通的實(shí)例創(chuàng)建行為通常是一個比較簡單的方法。但是我們還能否找到更優(yōu)雅的解決方案呢?

例如,你可能會考慮重新定義類的 __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

初看起來好像可以達(dá)到預(yù)期效果,但是問題是 __init__() 每次都會被調(diào)用,不管這個實(shí)例是否被緩存了。例如:

>>> s = Spam('Dave')
Initializing Spam
>>> t = Spam('Dave')
Initializing Spam
>>> s is t
True
>>>

這個或許不是你想要的效果,因此這種方法并不可取。

上面我們使用到了弱引用計數(shù),對于垃圾回收來講是很有幫助的,關(guān)于這個我們在8.23小節(jié)已經(jīng)講過了。當(dāng)我們保持實(shí)例緩存時,你可能只想在程序中使用到它們時才保存。一個 WeakValueDictionary 實(shí)例只會保存那些在其它地方還在被使用的實(shí)例。否則的話,只要實(shí)例不再被使用了,它就從字典中被移除了。觀察下下面的測試結(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)
[]
>>>

對于大部分程序而已,這里代碼已經(jīng)夠用了。不過還是有一些更高級的實(shí)現(xiàn)值得了解下。

首先是這里使用到了一個全局變量,并且工廠函數(shù)跟類放在一塊。我們可以通過將緩存代碼放到一個單獨(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)

這樣的話代碼更清晰,并且也更靈活,我們可以增加更多的緩存管理機(jī)制,只需要替代manager即可。

還有一點(diǎn)就是,我們暴露了類的實(shí)例化給用戶,用戶很容易去直接實(shí)例化這個類,而不是使用工廠方法,如:

>>> a = Spam('foo')
>>> b = Spam('foo')
>>> a is b
False
>>>

有幾種方式可以防止用戶這樣做,第一個是將類的名字修改為以下劃線(_)開頭,提示用戶別直接調(diào)用它。第二種就是讓這個類的 __init__() 方法拋出一個異常,讓它不能被初始化:

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() 來創(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é)中的元類實(shí)現(xiàn)的更優(yōu)雅一點(diǎn)(使用了更高級的技術(shù))。

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號