惰性模块变量——可以实现吗?

47 投票
9 回答
19438 浏览
提问于 2025-04-15 14:31

我正在寻找一种方法来懒加载一个模块级别的变量。

具体来说,我写了一个小的Python库,用来和iTunes进行交互,我想要一个叫做 DOWNLOAD_FOLDER_PATH 的模块变量。不幸的是,iTunes并不会告诉你它的下载文件夹在哪里,所以我写了一个函数,可以获取一些播客曲目的文件路径,然后向上查找目录,直到找到“Downloads”文件夹。

这个过程需要一两秒钟,所以我希望它在需要的时候再去计算,而不是在模块导入的时候就计算。

有没有办法在第一次访问这个模块变量时懒加载它,还是说我必须依赖一个函数呢?

9 个回答

13

我在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
    ...

这段代码可以放到另一个模块中,并且可以像上面的例子一样扩展属性。

也许这对某些人有用。

19

其实,从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}")
62

你不能直接用模块来实现这个功能,但你可以把一个类伪装成模块的样子。例如,在 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 里面去写!

撰写回答