跳转到主要内容

记忆元类。创建缓存的简单方法

项目描述

Travis CI build status PyPI Package latest release Supported versions Supported implementations Wheel packaging support Test line coverage Test branch coverage

一种快速的方法,使Python类自动根据它们被实例化的参数(即它们的__init__的参数)记忆化(即缓存)它们的实例。

这是一种避免反复创建昂贵对象并确保具有自然‘身份’的对象只创建一次的简单方法。如果您想要更复杂,mementos实现了多例模式

用法

假设您有一个名为Thing的类,该类需要昂贵的计算来创建,或者应该只创建一次。简单易行

from mementos import mementos

class Thing(mementos):

    def __init__(self, name):
        self.name = name

    ...

然后Thing对象将被记忆化

t1 = Thing("one")
t2 = Thing("one")
assert t1 is t2    # same instantiation args => same object

内部原理

当您定义一个类class Thing(mementos)时,它看起来像您正在继承mementos类。实际上不是。 mementos是一个元类,而不是超类。完整的表达式相当于class Thing(with_metaclass(MementoMetaclass, object)),其中with_metaclassMementoMetaclass也由mementos模块提供。

元类不是普通的超类;相反,它们定义了类是如何被构建的。实际上,它们定义了大多数类都不必定义的神秘 __new__ 方法。在这种情况下,mementos 实际上是在说,“嘿,在创建另一个对象之前,先在缓存中查找这个对象。”

如果你愿意,你可以使用带有完整 with_metaclass 规范的较长调用,但这并不是必需的,除非你定义自己的记忆化函数。更多内容将在下面介绍。

Python 2 与 Python 3

Python 2 和 3 在指定元类方面有不同的形式。在 Python 2 中

from mementos import MementoMetaclass

class Thing(object):

    __metaclass__ = MementoMetaclass  # now I'm memoized!

    ...

而 Python 3 使用

class Thing3(object, metaclass=MementoMetaclass):

    ...

mementos 支持这两种方式。但是 Python 2 和 Python 3 不互相识别对方的元类指定语法,所以一个版本的简单代码甚至无法在另一个版本中编译。上面显示的 with_metaclass() 函数是实现跨版本兼容性的方法。它与 six 跨版本兼容模块中的功能非常相似。

小心调用签名

MementoMetaclass 在调用签名上缓存,这在 Python 中可能会有很大的变化,即使是逻辑上相同的调用。这尤其适用于使用了关键字参数的情况。例如,def func(a, b=2): pass 可以被调用为 func(1)func(1, 2)func(a=1)func(1, b=2)func(a=2, b=2)。所有这些都解析为相同的逻辑调用——这仅仅是两个参数的情况!如果有多于一个的关键字参数,它们可以被任意排序,从而创建许多逻辑上相同的排列。

因此,如果你使用一个逻辑上相同的调用再次实例化一个对象,但使用不同的调用结构/签名,对象不会只被创建和缓存一次——它将被创建和缓存多次。

o1 = Thing("lovely")
o2 = Thing(name="lovely")
assert o1 is not o2     # because the call signature is different

这可能会降低性能,并且如果依赖 mementos 创建单个对象,还可能产生错误。所以不要这么做。使用一致的调用风格,就不会有问题。

在大多数情况下,这不是一个问题,因为对象通常使用有限的参数进行实例化,你可以确保它们使用平行的调用签名进行实例化。由于这99%的时间都能工作,并且实现简单,所以这种不完美的代价是值得的。

部分签名

如果你想只使用初始化时间调用签名的一部分(即 __init__ 的参数)来定义对象的身份/缓存键,有两种方法。一种方法是使用 MementoMetaclass 并设计没有冗余属性的 __init__,然后创建一个或多个辅助方法来添加/设置有用的但非必要的数据。例如:

class OtherThing(with_metaclass(MementoMetaclass, object)):

    def __init__(self, name):
        self.name = name
        self.color = None   # unset for now
        self.weight = None

    def set(self, color=None, weight=None):
        self.color = color or self.color
        self.weight = weight or self.weight
        return self

ot1 = OtherThing("one").set(color='blue')
ot2 = OtherThing("one").set(weight='light')
assert ot1 is ot2
assert ot1.color == ot2.color == 'blue'
assert ot1.weight == ot2.weight == 'light'

或者你可以定义自己的记忆化元类,使用下面描述的工厂函数。

访问工厂

mementos 的第一版定义了一个单个的元类。后来它被重新实现为一个参数化的元元类。酷吧?这意味着它定义了一个函数,memento_factory(),它接受一个元类名称和一个定义如何构建缓存键的函数作为参数,并返回一个相应的元类。MementoMetaclass 是模块预先定义的唯一元类,但很容易定义自己的记忆化元类。

from mementos import memento_factory, with_metaclass

IdTracker = memento_factory('IdTracker',
                            lambda cls, args, kwargs: (cls, id(args[0])) )

class MyTracker(with_metaclass(IdTracker, object)):
    ...

    # object identity is the object id of first argument to __init__
    # (and there must be one, else the args[0] reference => IndexError)

memento_factory() 的第一个参数是要定义的元类的名称。第二个参数是一个可调用的对象(例如 lambda 表达式或函数对象),它接受三个参数:一个类对象、一个参数 list 和一个关键字参数 dict。注意,没有 *** 魔法——传递给键函数的参数已经被解析为基本数据结构。

可调用函数必须返回一个全局唯一的、可哈希的对象键。这个键将被存储在 _memento_cache 中,它是一个简单的 dict

当使用各种参数作为缓存键/对象标识符时,您可以使用一个包含所需键的类和参数的 tuple。这也有助于调试,如果您需要直接检查 _memento_cache 缓存的话。但在像上面的 IdTracker 这样的情况下,您不需要保留额外的信息。原始 id(args[0]) 整数值就足够了,也可以使用构造的字符串或其他不可变、可哈希的值。

在参数非常灵活或涉及灵活数据类型的情况下,可以使用如 SuperHash 提供的高性能哈希函数。例如:

from superhash import superhash

SuperHashMeta = memento_factory('SuperHashMeta',
                            lambda cls, args, kwargs: (cls, superhash(args)) )

对于必须将多个调用变体最终解析为唯一规范签名的 1% 边缘情况,这可以基于特定的参数进行定制。或者在 Python 2.7 和 3.x 中,可以使用 inspect 模块的 getcallargs() 函数创建一个通用的“调用指纹”,用作键。(请参阅测试示例。)

注意

  • 有关扩展变更日志,请参阅 CHANGES.rst

  • mementosmemento 不同,后者执行完全不同的功能。

  • mementos 最初是由 Valentino Volonghi 在 ActiveState 菜谱 中提出的。虽然当前实现与该菜谱相当不同,范围也更广,但该菜谱的可用性使得这个模块以及依赖它的模块列表得以增长。这正是开源进化的全部意义。感谢,Valentino!

  • 可以安全地同时缓存多个类。它们都将存储在同一个缓存中,但它们的类是缓存键的一部分,因此值是不同的。

  • 该实现本身不是线程安全的。如果您处于多线程环境中,请考虑在对象实例化时使用锁。

  • 使用 pytestpytest-covcoveragetox 进行自动化多版本测试。使用 Travis-CI 进行持续集成测试。使用 pyroma 进行打包代码审查。

  • 作者 Jonathan Eunice@jeunice 欢迎您的评论和建议。

安装

要安装或升级到最新版本

pip install -U mementos

您可能需要在这些命令前加上 sudo 以授权安装。在没有超级用户权限的环境中,您可能想使用 pip--user 选项,只为单个用户安装,而不是全局安装。根据您的系统配置,您可能还需要使用单独的 pip2pip3 程序分别安装 Python 2 和 3。作为 pip 与您想运行的 Python 解释器之间关系的后备方案,您可以在特定的 Python 可执行文件下调用 pip 作为模块。

python3.6 -m pip install -U mementos

测试

要运行模块测试,请使用以下命令之一

tox                # normal run - speed optimized
tox -e py27        # run for a specific version only (e.g. py27, py34)
tox -c toxcov.ini  # run full coverage tests

项目详情


下载文件

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

源分布

mementos-1.3.1.zip (22.7 kB 查看哈希值)

上传时间

构建分布

mementos-1.3.1-py2.py3-none-any.whl (12.1 kB 查看哈希值)

上传时间 Python 2 Python 3

支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面