惰性模块变量——可以实现吗?
我正在寻找一种方法来懒加载一个模块级别的变量。
具体来说,我写了一个小的Python库,用来和iTunes进行交互,我想要一个叫做 DOWNLOAD_FOLDER_PATH
的模块变量。不幸的是,iTunes并不会告诉你它的下载文件夹在哪里,所以我写了一个函数,可以获取一些播客曲目的文件路径,然后向上查找目录,直到找到“Downloads”文件夹。
这个过程需要一两秒钟,所以我希望它在需要的时候再去计算,而不是在模块导入的时候就计算。
有没有办法在第一次访问这个模块变量时懒加载它,还是说我必须依赖一个函数呢?
9 个回答
我在Python 3.3上使用了Alex的实现,但结果非常糟糕:
这段代码
def __getattr__(self, name):
return globals()[name]
是不正确的,因为应该抛出一个AttributeError
错误,而不是KeyError
错误。
在Python 3.3下,这个错误会立刻出现,因为在导入时会进行很多检查,
寻找像__path__
、__loader__
这样的属性。
这是我们现在在项目中使用的版本,它允许在模块中进行懒加载。
模块的__init__
函数会被延迟到第一次访问一个没有特殊名称的属性时:
""" config.py """
# lazy initialization of this module to avoid circular import.
# the trick is to replace this module by an instance!
# modelled after a post from Alex Martelli :-)
class _Sneaky(object):
def __init__(self, name):
self.module = sys.modules[name]
sys.modules[name] = self
self.initializing = True
def __getattr__(self, name):
# call module.__init__ after import introspection is done
if self.initializing and not name[:2] == '__' == name[-2:]:
self.initializing = False
__init__(self.module)
return getattr(self.module, name)
_Sneaky(__name__)
这个模块现在需要定义一个init函数。这个函数可以用来导入可能会导入我们自己的模块:
def __init__(module):
...
# do something that imports config.py again
...
这段代码可以放到另一个模块中,并且可以像上面的例子一样扩展属性。
也许这对某些人有用。
其实,从Python 3.7开始,我们可以通过在模块级别定义一个 __getattr__()
函数来干这件事,这样做很干净利落。这个功能在 PEP 562 中有说明,并且在Python的参考文档的 数据模型章节 中也有详细记录。
# mymodule.py
from typing import Any
DOWNLOAD_FOLDER_PATH: str
def _download_folder_path() -> str:
global DOWNLOAD_FOLDER_PATH
DOWNLOAD_FOLDER_PATH = ... # compute however ...
return DOWNLOAD_FOLDER_PATH
def __getattr__(name: str) -> Any:
if name == "DOWNLOAD_FOLDER_PATH":
return _download_folder_path()
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
你不能直接用模块来实现这个功能,但你可以把一个类伪装成模块的样子。例如,在 itun.py
文件里,代码是这样的...
import sys
class _Sneaky(object):
def __init__(self):
self.download = None
@property
def DOWNLOAD_PATH(self):
if not self.download:
self.download = heavyComputations()
return self.download
def __getattr__(self, name):
return globals()[name]
# other parts of itun that you WANT to code in
# module-ish ways
sys.modules[__name__] = _Sneaky()
现在任何人都可以通过 import itun
来导入这个模块,实际上他们得到的是你的 itun._Sneaky()
实例。这里的 __getattr__
是用来让你访问 itun.py
中的其他内容,这样你就可以像使用顶层模块对象一样方便地编写代码,而不需要在 _Sneaky
里面去写!