8.25 创建缓存实例

问题

在创建一个类的对象时,如果之前使用同样参数创建过这个对象, 你想返回它的缓存引用。

解决方案

这种通常是因为你希望相同参数创建的对象时单例的。在很多库中都有实际的例子,比如 logging 模块,使用相同的名称创建的 logger 实例永远只有一个。例如:

  1. >>> import logging
  2. >>> a = logging.getLogger('foo')
  3. >>> b = logging.getLogger('bar')
  4. >>> a is b
  5. False
  6. >>> c = logging.getLogger('foo')
  7. >>> a is c
  8. True
  9. >>>

为了达到这样的效果,你需要使用一个和类本身分开的工厂函数,例如:

  1. # The class in question
  2. class Spam:
  3. def __init__(self, name):
  4. self.name = name
  5.  
  6. # Caching support
  7. import weakref
  8. _spam_cache = weakref.WeakValueDictionary()
  9. def get_spam(name):
  10. if name not in _spam_cache:
  11. s = Spam(name)
  12. _spam_cache[name] = s
  13. else:
  14. s = _spam_cache[name]
  15. return s

然后做一个测试,你会发现跟之前那个日志对象的创建行为是一致的:

  1. >>> a = get_spam('foo')
  2. >>> b = get_spam('bar')
  3. >>> a is b
  4. False
  5. >>> c = get_spam('foo')
  6. >>> a is c
  7. True
  8. >>>

讨论

编写一个工厂函数来修改普通的实例创建行为通常是一个比较简单的方法。但是我们还能否找到更优雅的解决方案呢?

例如,你可能会考虑重新定义类的 new() 方法,就像下面这样:

  1. # Note: This code doesn't quite work
  2. import weakref
  3.  
  4. class Spam:
  5. _spam_cache = weakref.WeakValueDictionary()
  6. def __new__(cls, name):
  7. if name in cls._spam_cache:
  8. return cls._spam_cache[name]
  9. else:
  10. self = super().__new__(cls)
  11. cls._spam_cache[name] = self
  12. return self
  13. def __init__(self, name):
  14. print('Initializing Spam')
  15. self.name = name

初看起来好像可以达到预期效果,但是问题是 init() 每次都会被调用,不管这个实例是否被缓存了。例如:

  1. >>> s = Spam('Dave')
  2. Initializing Spam
  3. >>> t = Spam('Dave')
  4. Initializing Spam
  5. >>> s is t
  6. True
  7. >>>

这个或许不是你想要的效果,因此这种方法并不可取。

上面我们使用到了弱引用计数,对于垃圾回收来讲是很有帮助的,关于这个我们在8.23小节已经讲过了。当我们保持实例缓存时,你可能只想在程序中使用到它们时才保存。一个 WeakValueDictionary 实例只会保存那些在其它地方还在被使用的实例。否则的话,只要实例不再被使用了,它就从字典中被移除了。观察下下面的测试结果:

  1. >>> a = get_spam('foo')
  2. >>> b = get_spam('bar')
  3. >>> c = get_spam('foo')
  4. >>> list(_spam_cache)
  5. ['foo', 'bar']
  6. >>> del a
  7. >>> del c
  8. >>> list(_spam_cache)
  9. ['bar']
  10. >>> del b
  11. >>> list(_spam_cache)
  12. []
  13. >>>

对于大部分程序而已,这里代码已经够用了。不过还是有一些更高级的实现值得了解下。

首先是这里使用到了一个全局变量,并且工厂函数跟类放在一块。我们可以通过将缓存代码放到一个单独的缓存管理器中:

  1. import weakref
  2.  
  3. class CachedSpamManager:
  4. def __init__(self):
  5. self._cache = weakref.WeakValueDictionary()
  6.  
  7. def get_spam(self, name):
  8. if name not in self._cache:
  9. s = Spam(name)
  10. self._cache[name] = s
  11. else:
  12. s = self._cache[name]
  13. return s
  14.  
  15. def clear(self):
  16. self._cache.clear()
  17.  
  18. class Spam:
  19. manager = CachedSpamManager()
  20. def __init__(self, name):
  21. self.name = name
  22.  
  23. def get_spam(name):
  24. return Spam.manager.get_spam(name)

这样的话代码更清晰,并且也更灵活,我们可以增加更多的缓存管理机制,只需要替代manager即可。

还有一点就是,我们暴露了类的实例化给用户,用户很容易去直接实例化这个类,而不是使用工厂方法,如:

  1. >>> a = Spam('foo')
  2. >>> b = Spam('foo')
  3. >>> a is b
  4. False
  5. >>>

有几种方式可以防止用户这样做,第一个是将类的名字修改为以下划线()开头,提示用户别直接调用它。第二种就是让这个类的 _init() 方法抛出一个异常,让它不能被初始化:

  1. class Spam:
  2. def __init__(self, *args, **kwargs):
  3. raise RuntimeError("Can't instantiate directly")
  4.  
  5. # Alternate constructor
  6. @classmethod
  7. def _new(cls, name):
  8. self = cls.__new__(cls)
  9. self.name = name

然后修改缓存管理器代码,使用 Spam._new() 来创建实例,而不是直接调用 Spam() 构造函数:

  1. # ------------------------最后的修正方案------------------------
  2. class CachedSpamManager2:
  3. def __init__(self):
  4. self._cache = weakref.WeakValueDictionary()
  5.  
  6. def get_spam(self, name):
  7. if name not in self._cache:
  8. temp = Spam3._new(name) # Modified creation
  9. self._cache[name] = temp
  10. else:
  11. temp = self._cache[name]
  12. return temp
  13.  
  14. def clear(self):
  15. self._cache.clear()
  16.  
  17. class Spam3:
  18. def __init__(self, *args, **kwargs):
  19. raise RuntimeError("Can't instantiate directly")
  20.  
  21. # Alternate constructor
  22. @classmethod
  23. def _new(cls, name):
  24. self = cls.__new__(cls)
  25. self.name = name
  26. return self

最后这样的方案就已经足够好了。缓存和其他构造模式还可以使用9.13小节中的元类实现的更优雅一点(使用了更高级的技术)。

原文:

http://python3-cookbook.readthedocs.io/zh_CN/latest/c08/p25_creating_cached_instances.html