使用装饰器获取json数据,如果文件存在,否则运行方法并将输出存储为json?

1 投票
4 回答
1817 浏览
提问于 2025-04-18 15:27

我对装饰器了解得不多,脑袋也不太灵光,但我觉得在这种情况下它们可能会派上用场。

我有一个主方法,它会运行其他一些方法:

def run_pipeline():

    gene_sequence_fasta_files_made = create_gene_sequence_fasta_files()

    ....several other methods taking one input argument and having one output argument.

因为每个方法运行起来都很耗时间,所以我想把每个方法的结果存储在一个json对象里。如果这个json文件存在,我就加载它;如果不存在,我就运行方法并保存结果。现在我的解决方案是这样的:

def run_pipeline():

    gene_sequence_fasta_files_made = _load_or_make(create_gene_sequence_fasta_files, "/home/myfolder/ff.json", method_input=None)

    ...

问题是,我觉得这个代码看起来很丑,读起来也很费劲。如果可以的话,我该如何用装饰器来解决这个问题呢?

补充一下,抱歉没展示我的尝试。我还没试过任何方法,因为我正在赶一个客户的截止日期,没时间去搞(我可以提供上面的代码;只是觉得它看起来不太好看)。

再补充一下,_load_or_make()的定义在这里:

def _load_or_make(method, filename, method_input=None):

    try:
        with open(filename, 'r') as input_handle:
            data = json.load(input_handle)

    except IOError:
        if method_input == None:
            data = method()
        else:
            data = method(method_input)

        with open(filename, 'w+') as output_handle:
            json.dump(data, output_handle)

    return data

4 个回答

0

我对Python的装饰器不是很懂,只是从教程上学来的。不过我觉得这可能对你有帮助。不过你可能不会觉得它更容易读懂。

装饰器是一种给不同函数提供相似解决方案的方法,这样可以处理事情,而不会让你的代码变得混乱或者失去可读性。它对你代码的其他部分来说就像是透明的一样。

def _load_or_make(filename):
   def _deco(method):
      def __deco():
         try:
            with open(filename, 'r') as input_handle:
                  data = json.load(input_handle)
                  return data
         except IOError:
            if method_input == None:
                  data = method()
            else:
                  data = method(method_input)

         with open(filename, 'w+') as output_handle:
                  json.dump(data, output_handle)

         return data
     return __deco
 return _deco

@_load_or_make(filename)
def method(arg):
    #things need to be done
    pass
    return data
2

装饰器其实就是一个可以被调用的东西,它接受一个函数(或者一个类)作为输入,然后对它做一些事情,最后返回一些东西(通常是一个被包装的函数,或者是被修改或注册的类):

因为我觉得“简单比复杂好”,所以如果装饰器比较复杂的话,我喜欢用类来实现:

class GetData(object):

    def __init__(self, filename):
        # this is called on the @decorator line
        self.filename = filename
        self.method_input = input

    def __call__(self, func):
        # this is called by Python with the completed def
        def wrapper(*args, **kwds):
            try:
                with open(self.filename) as stored:
                    data = json.load(stored)
            except IOError:
                data = func(*args, **kwds)
                with open(self.filename, 'w+') as stored:
                    json.dump(data, stored)
            return data
        return wrapper

然后在使用的时候:

@GetData('/path/to/some/file')
def create_gene_sequence_fasta_files('this', 'that', these='those'):
    pass

@GetData('/path/to/some/other/file')
def create_gene_sequence_fastb_files():
    pass
2

你想把结果保存到硬盘上,还是说保存在内存里就可以?如果可以的话,你可以使用一个叫做 memoize 的装饰器/模式,具体内容可以在这里找到:https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize

这个方法会把每一组独特的输入参数的结果保存在内存中。如果之后用相同的参数再调用这个函数,它会直接从内存中返回结果,而不是重新运行这个函数。

这个方法还可以调整,让它在一定时间后失效(这取决于你的程序运行多久),这样如果在某个时间之后再次调用,就会重新运行函数并更新结果。

4

这里有一个装饰器,它会尝试从给定的 filename 加载 json 文件。如果找不到这个文件,或者加载 json 失败,它就会运行原来的函数,把结果以 json 格式写入磁盘,然后返回结果。

def load_or_make(filename):
    def decorator(func):
        def wraps(*args, **kwargs):
            try:
                with open(filename, 'r') as f:
                    return json.load(input_handle)
            except Exception:
                data = func(*args, **kwargs)
                with open(filename, 'w') as out:
                    json.dump(data, out)
                return data
        return wraps
    return decorator

@load_or_make(filename)
def your_method_with_arg(arg):
    # do stuff
    return data

@load_or_make(other_filename)
def your_method():
    # do stuff
    return data

需要注意的是,这种方法有个问题:如果被装饰的方法根据传入的参数返回不同的值,缓存就会出现问题。看起来这对你来说不是个要求,但如果是的话,你需要根据传入的参数选择不同的文件名(或者使用 pickle 这种序列化方式,直接把参数和结果放到一个字典里)。下面是一个使用 pickle 方法的例子,灵感来自于 Christian P. 提到的 memoized 装饰器

import pickle

def load_or_make(filename):
    def decorator(func):
        def wrapped(*args, **kwargs):
            # Make a key for the arguments. Try to make kwargs hashable
            # by using the tuple returned from items() instead of the dict
            # itself.
            key = (args, kwargs.items())
            try:
                hash(key)
            except TypeError:
                # Don't try to use cache if there's an
                # unhashable argument.
                return func(*args, **kwargs)
            try:
                with open(filename) as f:
                    cache = pickle.load(f)
            except Exception:
                cache = {}
            if key in cache:
                return cache[key]
            else:
                value = func(*args, **kwargs)
                cache[key] = value
                with open(filename, "w") as f:
                    pickle.dump(cache, f)
                return value
        return wrapped
    return decorator

在这里,我们不是把结果保存为 json,而是把结果作为字典中的一个值进行 pickle,其中键是传递给函数的参数。请注意,你仍然需要为每个被装饰的函数使用不同的 filename,以确保缓存不会返回错误的结果。

撰写回答