跳转到主要内容

Kids缓存库。

项目描述

Latest PyPI version Number of PyPI downloads Travis CI build status Test coverage

kids.cache 是一个Python库,提供缓存装饰器。它是‘Kids’(为了保持简单)库的一部分。它不依赖于任何Python库。

其主要关注点是提供一种非常简单的默认使用方案,同时不忘在需要时提供完整的功能。

成熟度

这段代码大约有100行Python代码,并且有100%的测试覆盖率。

然而,目前它仍然被认为是beta阶段。

兼容性

它很小很简单,应该可以在任何地方工作。

更详细地说:当前的代码足够简单,它使用了Python的公共子集,兼容任何平台上的Python 2.7和Python >= 3……而且没有任何特定的修改。

即使如此,您可能会很高兴地知道,这段代码已经针对Python 2.7、3.4、3.5、3.6在Linux和Windows平台上进行了每个提交的兼容性测试。

特性

  • 通过一个简单的调用 @cache,就可以消除大部分隐藏的复杂性。

    • 在您可以将装饰器(函数、方法、属性、类等)粘附的所有地方都可以正常工作。

    • 支持在 @property@classmethod@staticmethod 等常用装饰器之前或之后调用。

  • 使用 @cache 可以实现几个设计模式

    • memoization 当用于带参数的函数时。

    • lazy evaluation 当放置在属性上时。

    • singleton 模式,当放置在类上时。

  • 提供全面的自定义

    • 缓存清除或缓存统计功能。

    • 支持来自cachetools包的任何缓存存储机制。

    • 支持自定义键函数,允许

      • 支持您的外来不可哈希对象

      • 微调哪些函数调用可以被认为是相同的

      • 在对象中手动选择函数依赖(对于方法)

基本用法

函数

此缓存装饰器非常简单易用

>>> from kids.cache import cache

>>> @cache
... def answer_to_everything():
...     print("many insightfull calculation")
...     return 42

然后,函数answer_to_everything仅在第一次调用时执行计算,并保存结果,在后续调用时直接返回它

>>> answer_to_everything()
many insightfull calculation
42

>>> answer_to_everything()
42

函数体不再执行,而是使用缓存值。

它可以与参数一起工作

>>> @cache
... def mysum(*args):
...     print("calculating...")
...     return sum(args)

>>> mysum(2, 2, 3)
calculating...
7
>>> mysum(1, 1, 1, 1)
calculating...
4
>>> mysum(2, 2, 3)
7
>>> mysum(1, 1, 1, 1)
4

请注意,默认情况下,对象是无类型的,因此

>>> mysum(1.0, 1, 1, 1)
4

触发了缓存,尽管第一个参数是一个浮点数而不是整数。

方法

使用方法

>>> class MyObject(object):
...    def __init__(self, a, b):
...        self.a, self.b = a, b
...
...    @cache
...    def total(self):
...        print("calculating...")
...        return self.a + self.b

>>> xx = MyObject(2, 3)
>>> xx.total()
calculating...
5
>>> xx.total()
5

缓存不会在实例之间共享

>>> yy = MyObject(2, 3)
>>> yy.total()
calculating...
5

当然,如果您更改实例的内部值,缓存方法将不会检测到这一点

>>> xx.a = 5
>>> xx.total()
5

查看高级用法以了解如何更改这些行为的一些方法。

属性

您可以使用cache装饰器与属性一起使用,并为具有延迟评估属性的属性提供了一种很好的方式

>>> class WithProperty(MyObject):
...
...    @property
...    @cache
...    def total(self):
...        print("evaluating...")
...        return self.a + self.b

>>> xx = WithProperty(1, 1)
>>> xx.total
evaluating...
2
>>> xx.total
2

您可以在@cache装饰器之前或之后使用@property装饰器

>>> class WithProperty(MyObject):
...
...    @cache
...    @property
...    def total(self):
...        print("evaluating...")
...        return self.a + self.b

>>> xx = WithProperty(2, 2)
>>> xx.total
evaluating...
4
>>> xx.total
4

类方法

您可以使用cache装饰器与类方法一起使用,并为实例之间共享缓存提供了一种很好的方式

>>> class WithClassMethod(MyObject):
...
...    a = 2
...    b = 3
...
...    @classmethod
...    @cache
...    def total(cls):
...        print("evaluating...")
...        return cls.a + cls.b

>>> WithClassMethod.total()
evaluating...
5
>>> WithClassMethod.total()
5

您可以在@cache装饰器之前或之后使用@property装饰器

>>> class WithClassMethod(MyObject):
...
...    a = 1
...    b = 6
...
...    @cache
...    @classmethod
...    def total(cls):
...        print("evaluating...")
...        return cls.a + cls.b

>>> WithClassMethod.total()
evaluating...
7
>>> WithClassMethod.total()
7

静态方法

您可以使用cache装饰器与静态方法一起使用

>>> class WithStaticMethod(MyObject):
...
...    @staticmethod
...    @cache
...    def total(a, b):
...        print("evaluating...")
...        return a + b

>>> WithStaticMethod.total(1, 3)
evaluating...
4
>>> WithStaticMethod.total(1, 3)
4

您可以在@cache装饰器之前或之后使用@property装饰器

>>> class WithStaticMethod(MyObject):
...
...    @cache
...    @staticmethod
...    def total(a, b):
...        print("evaluating...")
...        return a + b

>>> WithStaticMethod.total(2, 6)
evaluating...
8
>>> WithStaticMethod.total(2, 6)
8

使用cache与类将允许围绕单例的概念进行变化。单例在内存中共享相同的id,因此这显示了经典的非单例行为

>>> a, b = object(), object()
>>> id(a) == id(b)
False

基于工厂的单例

您可以使用cache装饰器与类一起使用,实际上实现了创建单例的工厂模式

>>> @cache
... class MySingleton(MyObject):
...     def __new__(cls):
...         print("instanciating...")
...         return MyObject.__new__(cls)
...     def __init__(self):
...         print("initializing...")

>>> a, b = MySingleton(), MySingleton()
instanciating...
initializing...
>>> id(a) == id(b)
True

请注意,这两个实例都是相同的对象,因此它只被实例化和初始化了一次。

但请注意:这不再是类了

>>> MySingleton
<function MySingleton at ...>

基于实例的单例

略有不同,可以通过缓存__new__来实现类单例模式

>>> class MySingleton(MyObject):
...     @cache
...     def __new__(cls):
...         print("instanciating...")
...         return MyObject.__new__(cls)
...     def __init__(self):
...         print("initializing...")

>>> a, b = MySingleton(), MySingleton()
instanciating...
initializing...
initializing...
>>> id(a) == id(b)
True

请注意,这两个实例都是相同的对象,因此它只被实例化了一次。但__init__被调用了两次。这有时是完全可以接受的,但您可能想避免这种情况。

所以,如果您不想这样做,您应该也缓存__init__方法

>>> class MySingleton(MyObject):
...     @cache
...     def __new__(cls):
...         print("instanciating...")
...         return MyObject.__new__(cls)
...     @cache
...     def __init__(self):
...         print("initializing...")

>>> a, b = MySingleton(), MySingleton()
instanciating...
initializing...
>>> id(a) == id(b)
True

当然,在这两种情况下,您都会保留完整的对象不受影响

>>> MySingleton
<class 'MySingleton'>

带参数的单例

实际上,只有在您以相同的参数连续调用它们时,这些才是单例。

更准确地说,当它们的实例化参数相同时,您可以共享您的类

>>> @cache
... class MySingleton(MyObject):
...     def __init__(self, a):
...         self.a = a
...         print("evaluating...")

>>> a, b = MySingleton(1), MySingleton(2)
evaluating...
evaluating...
>>> id(a) == id(b)
False

但是

>>> c = MySingleton(1)
>>> id(a) == id(c)
True

如果您想要一个即使在连续调用中不同也能给出相同实例的单例,您应该查看高级用法部分和key参数。

高级用法

大多数高级用法意味着要以参数调用@cache装饰器。请注意

>>> @cache
... def mysum1(*args):
...     print("calculating...")
...     return sum(args)

或者

>>> @cache()
... def mysum2(*args):
...     print("calculating...")
...     return sum(args)

是等价的

>>> mysum1(1,1)
calculating...
2
>>> mysum1(1,1)
2

>>> mysum2(1,1)
calculating...
2
>>> mysum2(1,1)
2

提供键函数

提供键函数可以非常强大,并允许您精确控制何时重新计算缓存。

哈希函数将接收与主函数调用完全相同的参数。它必须返回一个可哈希的结构(tuplesintstring等的组合…避免列表、字典和集合)。这将唯一标识结果。

例如您可以

>>> class WithKey(MyObject):
...    @cache(key=lambda s: (id(s), s.a, s.b))
...    def total(self):
...        print("calculating...")
...        return self.a + self.b

>>> xx = WithKey(2, 3)
>>> xx.total()
calculating...
5
>>> xx.total()
5

它应该检测实例给定值的更改

>>> xx.a = 5
>>> xx.total()
calculating...
8

而不必担心其他值更改时的重新计算

>>> xx.c = 7
>>> xx.total()
8

但它应该仍然在实例之间做出区别

>>> yy = WithKey(2, 3)
>>> yy.total()
calculating...
5

这个最后的例子很重要,因为您可能希望所有实例之间共享缓存。您可以通过在键函数中避免返回id(s)轻松实现这一点。

类型键函数

您可以要求typed参数不被视为相同

>>> @cache(typed=True)
... def mysum(*args):
...     print("calculating...")
...     return sum(args)
>>> mysum(1, 1)
calculating...
2

>>> mysum(1.0, 1)
calculating...
2.0

默认键函数

如果没有提供默认键函数,将尝试创建一个listdictset也将可作为键,尽管这些不是可哈希的。

键函数的名称为hippie_hashing,这是键参数的默认值

>>> from kids.cache import hippie_hashing

>>> @cache(key=hippie_hashing)
... def mylength(obj):
...     return len(obj)

这允许您使用函数与列表、字典或这些的组合

>>> mylength([set([3]), 2, {1: 2}])
3

即使您的对象也可以用作键,只要它们是可哈希的

>>> class MyObj(object):  ## object subclasses have a default hash
...     length = 5
...     def __len__(self, ):
...         print('calculating...')
...         return self.length

>>> myobj = MyObj()
>>> mylength(myobj)
calculating...
5

>>> mylength(myobj)
5

请放心,哈希冲突(它们会发生!)不会生成缓存冲突

>>> class MyCollidingHashObj(MyObj):
...     def __init__(self, length):
...          self.length = length
...     def __hash__(self):
...          return 1

>>> hash_collide1 = MyCollidingHashObj(6)
>>> hash_collide2 = MyCollidingHashObj(7)

>>> mylength(hash_collide1)
calculating...
6
>>> mylength(hash_collide2)
calculating...
7

但为了性能考虑,尽量避免它们!并且您可能应该意识到,如果您的对象相等,则将存在缓存冲突(但在这个时候,这可能正是您想要的,哈哈?)

>>> class MyEqCollidingHashObj(MyCollidingHashObj):
...     def __eq__(self, value):
...          return True
...     def __hash__(self):
...          return 1

>>> eq_and_hash_collide1 = MyEqCollidingHashObj(8)
>>> eq_and_hash_collide2 = MyEqCollidingHashObj(9)

>>> mylength(eq_and_hash_collide1)
calculating...
8
>>> mylength(eq_and_hash_collide2)
8

哎呀。这可能不是在这个例子中预期的,但您真的必须努力工作才能实现这一点。大多数时候,您可能会发现这很方便,并且会利用它。这有点像对象责任的key机制的扩展。

当然,hippie_hashing在特殊不可哈希的对象上会失败

>>> class Unhashable(object):
...    def __hash__(self):
...        raise ValueError("unhashable!")

>>> hippie_hashing(Unhashable())  ## doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: <Unhashable ...> can not be hashed. Try providing a custom key function.

如果您不是嬉皮士,您应该考虑使用strict=True,并将使用更有限的方法来从您的参数中生成键

>>> @cache(strict=True)
... def mylength(obj):
...     return len(obj)

>>> mylength("hello")
5

但那时,如果它与字典、列表或集合参数一起失败,请不要感到惊讶

>>> mylength([set([3]), 2, {1: 2}])
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'

您可以将typed=Truestrict=True结合使用

>>> @cache(strict=True, typed=True)
... def mysum(*args):
...     print("calculating...")
...     return sum(args)
>>> mysum(1, 1)
calculating...
2

>>> mysum(1.0, 1)
calculating...
2.0

一个好的键函数可以

  • 设置一些缓存超时(但您应该查看缓存存储部分以限制缓存的大小)

  • 精细选择与该方法相关的参数,以避免在非必要的情况下重新评估函数。

  • 允许您缓存具有非常特殊参数的可调用对象,这些参数无法正确哈希。

清理缓存

kids.cache使用了Python 3实现中的一些lru_cache思想,每个缓存的函数都收到一个cache_clear方法

>>> @cache
... def mysum(*args):
...     print("calculate...")
...     return sum(args)

>>> mysum(1,1)
calculate...
2
>>> mysum(1,1)
2

通过调用cache_clear方法,我们可以清除所有之前的缓存值

>>> mysum.cache_clear()
>>> mysum(1,1)
calculate...
2

缓存统计

kids.cache使用了Python 3实现中的一些lru_cache思想,每个缓存的函数都收到一个cache_info方法

>>> @cache
... def mysum(*args):
...     print("calculate...")
...     return sum(args)

>>> mysum(1,1)
calculate...
2
>>> mysum(1,1)
2

>>> mysum.cache_info()
CacheInfo(type='dict', hits=1, misses=1, maxsize=None, currsize=1)

缓存存储

kids.cache可以使用任何类似于字典的结构作为缓存存储。这意味着您可以提供一些更聪明的缓存存储。例如,您可以使用底层的cachetools缓存来管理缓存存储。

请记住,默认的缓存存储是…一个字典!如果您的程序将运行很长时间,并且您缓存了在整个运行期间将不同的函数调用,那么这不是一个好主意:缓存存储将随着每个新调用的到来而增长,使您的进程的内存使用量增长……也许超出了界限。

在这种情况下,您必须考虑使用受管理的缓存存储,它将清理和删除旧的未使用缓存条目。在cachetoolskids.cache中提供了许多缓存存储,并且kids.cache支持它们。

如果您需要来自 cachetools 的任何缓存存储,您可以提供它

>>> from cachetools import LRUCache

LRU 代表最近最少使用…

>>> @cache(use=LRUCache(maxsize=2))
... def mysum(*args):
...     print("calculate...")
...     return sum(args)

>>> mysum(1, 1)
calculate...
2
>>> mysum(1, 2)
calculate...
3
>>> mysum(1, 3)
calculate...
4

我们已经超过缓存内存,最近最少使用的已丢弃

>>> mysum(1, 1)
calculate...
2

但我们仍然有一个在内存中

>>> mysum(1, 3)
4

贡献

任何建议或问题都欢迎。非常欢迎推送请求,请查看指南。

推送请求指南

您可以发送任何代码。我会查看它,并自己将其集成到代码库中,并保留您为作者。这个过程可能需要时间,如果您遵循以下指南,则所需时间会更少。

  • 使用 PEP8 或 pylint 检查您的代码。尽量保持每行宽度为 80 列。

  • 按最小关注点分别提交。

  • 每个提交应通过测试(以便轻松的二分法)

  • 每个功能/错误修复提交应包含代码、测试和文档。

  • 优先处理带有排版或代码外观更改的较小提交。这些应该在提交摘要中使用 !minor 标记。

  • 提交消息应遵循 gitchangelog 规则(检查 git 日志以获取示例)

  • 如果提交修复了问题或完成了功能的实现,请在摘要中说明。

如果您对这里未回答的指南有任何疑问,请检查当前的 git log,您可能会找到以前提交的示例,说明如何处理您的问题。

许可证

版权 (c) 2017 Valentin Lab。

BSD 许可证 下授权。

变更日志

0.0.7 (2017-11-16)

修复

  • 修复了生成的变更日志与 README.rst 之间的 ReST 不一致。 [Valentin Lab]

    这阻止了 PyPI 页面正确渲染。

0.0.6 (2017-11-16)

修复

  • 修复了由于过时的命名空间模式导致的导入时间性能问题。(修复 #9) [Valentin Lab]

0.0.4 (2015-04-27)

新增

  • 支持在 staticmethod 装饰器之前或之后调用。 [Valentin Lab]

  • 支持在 classmethod 装饰器之前或之后调用。 [Valentin Lab]

变更

  • 记录了在 class 中使用时单例模式的使用。 [Valentin Lab]

0.0.3 (2015-02-24)

修复

  • 如果两个自定义对象共享相同的哈希和类型但不是 equal,则会出现讨厌的缓存冲突。 [Valentin Lab]

    事实上,这种情况确实发生了。例如,所有 object 的实例或任何子类都将继承一个使用 id 的特殊 hash 方法,但在某些版本的 python(最新版本)中,id 值被除以 16。并且,由于哈希冲突是不可避免的,因此不应导致缓存冲突。

0.0.2 (2015-02-02)

新增

  • 将类型添加到缓存统计信息中,删除了对 cachetools 的依赖。 [Valentin Lab]

变更

  • 默认缓存存储的 currsize 使用 len() 而不是 None。 [Valentin Lab]

    这对于默认字典实现来说是合理的。

修复

  • 错误归因于 cache_clearcache_info 函数。 [Valentin Lab]

  • 相似的 set 可能得到不同的哈希值。 [Valentin Lab]

    在哈希之前未对 set 进行排序。

0.0.1 (2014-05-23)

  • 第一次导入。 [Valentin Lab]

项目详情


下载文件

下载您平台上的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源代码发行版

kids.cache-0.0.7.tar.gz (20.1 kB 查看哈希值)

上传时间: 源码

构建版本

kids.cache-0.0.7-py2.py3-none-any.whl (18.0 kB 查看哈希值)

上传时间: Python 2 Python 3