创建缓存实例
解决问题
当创建类实例时,返回一个缓存引用
让其指向上一个用同样参数(如果有的话)创建出的类实例
解决方案
该问题常常出现在我们想确保针对某一组输入参数只会有一个类实例存在时,现实中的例子包括一些库的行为。
在logging模块中,给定一个名称只会关联到一个单独的logger实例。
>>> import logging
>>> a = logging.getLogger('foo')
>>> b = logging.getLogger('bar')
>>> a is b
False
>>> c = logging.getLogger('foo')
>>> a is c
True
如何实现
使用一个与类本身相分离的工厂函数
# The class in question
class Spam:
def __init__(self, name):
self.name = name
# Caching support
import weakref
"""
weakref : weak references
一个对象的弱引用不能保证对象存活:当对像的引用只剩弱引用时, garbage collection 可以销毁引用并将其内存重用于其他内容。但是,在实际销毁对象之前,即使没有强引用,弱引 用也一直能返回该对象。
弱引用的主要用途是实现保存大对象的高速缓存或映射,但又并希望大对象仅仅因为它出现在高 速缓存或映射中而保持存活。
"""
# https://docs.python.org/3/library/weakref.html
_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
>>> s = get_spam('lin')
>>> b = get_spam('qing')
>>> a is b
False
>>> c = get_spam('lin')
>>> a is c
True
"""
总结:结果类似logger的效果一样
"""
讨论
要想修改实例创建的规则,编写一个特殊的工厂函数常常是一种简单的方法。
此时,一个常被提到的问题就是是否可以用更优雅的方式来完成呢?
eg: 重新定义类的__new__()
方法
import weakref
class Spam:
_spam_cache = weakref.WeakValueDictionary()
def __new__(cls, name):
if name in cls_spam_cache:
return cls._spam_cache[nmae]
else:
self = super().__new__(cls)
cls._spam_cache[name] = self
return self
def __init__(self, name):
print("Init Spam")
self.name = name
# 乍一看代码,上面的代码似乎可以完成任务,但是,主要问题在于__init__() 方法总是会得到调用,无论对象实例有无得到缓存都是如此。
>>> t = Spam('Lin')
Init Spam
>>> s = Spam('Lin')
Init Spam
>>> s is t
"""
这种行为很可能不是想要的。
因此,要解决实例缓存后会重复初始化的问题,需要采用一个稍有些不同的方法。
本节中对弱引用的运用与垃圾收集有着极为重要的关系。
当维护实例缓存时,只要在程序中实际用到了他们,那么通常希望将对象保存在缓存中。
WeakValueDictionary 会保存着那些被引用的对象,只要它们存在于程序中的某处即可。
否则,当实例不在被使用时,字典的键就会消失。
如下示例:
"""
>>> a = get_spam('xiao')
Init Spam
>>> b = get_spam('qing')
Init Spam
>>> c = get_spam('lin')
Init Spam
>>> list(_spam_cache)
['xiao', 'qing', 'lin']
>>> del a
>>> list(_spam_cache)
['qing', 'lin']
>>> del b
>>> list(_spam_cache)
['lin']
>>> del c
>>> list(_spam_cache)
[]
对于很多程序而言,使用本节中给出的框架代码通常就足够了。
BUT,还可以使用更高级的实现技术
以上的解决方案需要依赖全局变量以及一个原始的类定义相分离的工厂函数。
"""
一种改进方式:将缓存的代码放到另一个单独的管理类中,然后将这些组件相粘合在一起
"""
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)
"""
总结:这种方法的特点就是为潜在的灵活性提供了更多的支持。
例如:我们可以实现不同类型的缓存管理机制(以单独的类来实现),
然后附加到Spam类中替换掉默认的缓存实现。
eg: 其它的代码(get_spam)不需要修改就能正常工作了
"""
"""
另一种设计上的考虑是到底要不要将类的定义暴露给用户。
如果什么都不做的话,用户可以很容易创建出实例,从而绕过缓存机制。
"""
>>> a = Spam('foo')
>>> b = Spam('foo')
>>> a is b
False
"""
如果预防出现这种行为对程序而言很重要,我们可以采取特定的步骤来避免。
eg: 类名前加一个下划线(_Spam),这样至少可以提醒用户不应该直接去访问它。
或者 如果想用户提供更强的提示,暗示不应该直接实例化Spam对象,可以让__init__()方法抛出一个异常,然后用一个类方法来实现构造函数的功能。
"""
# 代码实现
class Spam:
def __init__(self, *args, **kwargs):
raise RuntimeError("Can not instantiate directly")
# Alternate Constructor
@classmethod
def _new(cls, name):
self = cls.__new__(cls)
self.nmae = name
# 如何实现实例化,而不是使用通常所见的Spam()
>>> s = Spam._new('xiao')
# 修改缓存机制中的代码
class CacheSpamManager:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get_spam(self, name):
if name not in self._cache:
Spam._new(name) = s
else:
s = self._cache[name]
return s
"""
通过使用元类,缓存机制以及其它的创建模式(creational pattern)通常能够以更加优雅的方式得以解决
"""