使用装饰器来持久化python对象

时间:2016-06-17 13:35:20

标签: python ipython-notebook jupyter-notebook

我从下面链接获得的代码,可以将数据保存到磁盘。

http://tohyongcheng.github.io/python/2016/06/07/persisting-a-cache-in-python-to-disk.html

我尝试了但文件没有生成。

import atexit
import pickle
# or import cPickle as pickle

def persist_cache_to_disk(filename):
    def decorator(original_func):
        try:
            cache = pickle.load(open(filename, 'r'))
        except (IOError, ValueError):
            cache = {}

        atexit.register(lambda: pickle.dump(cache, open(filename, "w")))

        def new_func(*args):
            if tuple(args) not in cache:
                cache[tuple(args)] = original_func(*args)
            return cache[args]

        return new_func

    return decorator

我尝试按照示例使用此代码...

@persist_cache_to_disk('users.p')
def get_all_users():
    x = 'some user'
    return x

更新

这是在python命令提示符下工作,但在ipython notebook中不起作用。

2 个答案:

答案 0 :(得分:11)

问题是该示例使用atexit仅在python退出时运行转储例程。每次更新缓存时,此修改版本都将转储:

import atexit
import functools
import pickle
# or import cPickle as pickle

def persist_cache_to_disk(filename):
    def decorator(original_func):
        try:
            cache = pickle.load(open(filename, 'r'))
        except (IOError, ValueError):
            cache = {}

        # Your python script has to exit in order to run this line!
        # atexit.register(lambda: pickle.dump(cache, open(filename, "w")))
        #
        # Let's make a function and call it periodically:
        #
        def save_data():                                                        
            pickle.dump(cache, open(filename, "w"))  

        # You should wrap your func
        @functools.wraps(original_func)
        def new_func(*args):
            if tuple(args) not in cache:
                cache[tuple(args)] = original_func(*args)
                # Instead, dump your pickled data after
                # every call where the cache is changed.
                # This can be expensive!
                save_data()
            return cache[args]

        return new_func

    return decorator


@persist_cache_to_disk('users.p')
def get_all_users():
    x = 'some user'
    return x

get_all_users()

如果您想限制保存,可以修改save_data()仅保存,例如,当len(cache.keys())是100的倍数时。

我还为你的装饰者添加了functools.wraps。来自docs

  

如果不使用这个装饰工厂,示例函数的名称就是'包装',原始example()的docstring就会丢失。

答案 1 :(得分:5)

最佳解决方案取决于用例。没有一般方法可以立即解决所有问题。

缓存数据

如果要加速函数调用,可能需要将结果缓存在内存中(因为磁盘读/写速度也很慢)。如果要调用具有相同参数的函数,则自上次启动Python解释器以来的第一次调用将变慢。所有后续调用都将访问缓存(如果您的缓存足够大以存储所有结果)。

使用Python> = 3.2甚至还有一个内置装饰器@functools.lru_cache(maxsize=100, typed=False)

Decorator用一个memoizing callable来包装一个函数,它可以节省maxsize最近的调用。当使用相同的参数定期调用昂贵的或I / O绑定函数时,它可以节省时间。

示例:

@lru_cache(maxsize=32)
def get_pep(num):
    'Retrieve text of a Python Enhancement Proposal'
    resource = 'http://www.python.org/dev/peps/pep-%04d/' % num
    try:
        with urllib.request.urlopen(resource) as s:
            return s.read()
    except urllib.error.HTTPError:
        return 'Not Found'

>>> for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
...     pep = get_pep(n)
...     print(n, len(pep))

>>> get_pep.cache_info()
CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)

有一个backport for Python 2.7 on pypicachetools包,它也兼容Python 2.7,还包含Python 3 @ functools.lru_cache函数装饰器的变体。

磁盘上的持久数据

如果要在Python过程完成后保留数据,则将数据存储在磁盘上是有意义的。这可能会加快第一个函数调用,但它可能会减慢所有其他函数调用,因为它需要读取和写入文件。

@ rrauenza的解决方案看起来不错。有一些小的改进:

import pickle
import functools
import collections
# or import cPickle as pickle

def persist_cache_to_disk(filename):
    def decorator(original_func):
        try:
            cache = pickle.load(open(filename, 'r'))
        except (IOError, ValueError):
            cache = {}

        def save_data():
            pickle.dump(cache, open(filename, "w"))

        @functools.wraps(original_func)
        def new_func(*args):
            try:
                try:
                    hash(args)
                except TypeError:
                    # do not use cache because we cannot hash args
                    return original_func(*args)

                if tuple(args) not in cache:
                    cache[tuple(args)] = original_func(*args)
                    # dump complete cache,  this can be expensive!
                    save_data()
                return cache[args]
        return new_func

    return decorator

函数调用也在内存中缓存,类似于@ functools.lru_cache(),但它没有实现最大缓存大小(程序内存使用的潜在问题),也没有类似typed选项的内容(见上文)。

不幸的是shelve(由@Aya建议)不能直接使用,因为只支持字符串作为键。这应该会带来更好的性能,因为它不需要在每次更新时都写入完整的缓存。

如果用例不是缓存,那么Pickle不是首选的方法,而是在Python解释器启动之间存储数据。如果您必须更改腌制对象的类,则腌制文件将变得无用。在这种情况下可以清除缓存,但在其他情况下,请考虑使用yml,json或xml,或者如果您有大量数据,则使用某种二进制格式(例如hdf5)。

陷阱

  1. 并非所有参数都可以使用
  2. 所有参数都必须是可清除的。例如,列表和词典不可清除。对此没有简单而通用的解决方案。仔细考虑需要支持哪种参数。列表可以轻松转换为元组。也适用于字典可以制作。不幸的是,这适用于上面的所有缓存方法(包括内置的@ functools.lru_cache)。

    1. 并非所有返回值都可以选择
    2. 需要将数据序列化以存储在磁盘上。这通常通过使用pickle模块来完成。 shelve内部也使用pickle。不幸的是not every object can be pickled。如果函数包含不可选择的对象,您可以尝试使它们成为可选择的,或者选择不同的方式来序列化数据(以及用于存储序列化数据的不同文件格式)。如果你使用numpy对象numnpy.save()是一种非常快速的方法来存储大量数据。

      1. 类型信息丢失
      2. 对象可能相同,但不是同一类型。如果你的函数还取决于输入参数的类型,你可能会遇到麻烦:

        @functools.lru_cache(typed=False)
        def fun_with_numbers(a, b):
            return a/b, isinstance(3, float)
        

        division fails only with Python 2

        >>> fun_with_numbers(1, 3)
        0, False
        >>> fun_with_numbers(1., 3.)
        0, False
        

        使用@ functools.lru_cache(),你可以通过设置typed=True来解决这个问题,但是如果你使用不同的缓存方法,你可能需要自己实现类似的东西。

        1. 功能不仅仅依赖于输入参数
        2. 由于显而易见的原因,该函数不应依赖于非常量全局变量或其他外部参数。如果函数返回time.time(),它将始终返回第一个函数调用的缓存时间。

          1. 线程安全性
          2. 如果在没有正确锁定的情况下同时使用缓存函数,则会发生非常糟糕的事情。

            1. 你真的需要吗?
            2. 在添加缓存之前和之后,您应该profiling。如果代码很快,缓存可能会降低代码的速度。