使用装饰器获取json数据,如果文件存在,否则运行方法并将输出存储为json?
我对装饰器了解得不多,脑袋也不太灵光,但我觉得在这种情况下它们可能会派上用场。
我有一个主方法,它会运行其他一些方法:
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 个回答
我对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
装饰器其实就是一个可以被调用的东西,它接受一个函数(或者一个类)作为输入,然后对它做一些事情,最后返回一些东西(通常是一个被包装的函数,或者是被修改或注册的类):
因为我觉得“简单比复杂好”,所以如果装饰器比较复杂的话,我喜欢用类来实现:
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
你想把结果保存到硬盘上,还是说保存在内存里就可以?如果可以的话,你可以使用一个叫做 memoize
的装饰器/模式,具体内容可以在这里找到:https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize
这个方法会把每一组独特的输入参数的结果保存在内存中。如果之后用相同的参数再调用这个函数,它会直接从内存中返回结果,而不是重新运行这个函数。
这个方法还可以调整,让它在一定时间后失效(这取决于你的程序运行多久),这样如果在某个时间之后再次调用,就会重新运行函数并更新结果。
这里有一个装饰器,它会尝试从给定的 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
,以确保缓存不会返回错误的结果。