懒惰模块变量 - 可以实现吗?

46

我正在尝试找到一种延迟加载模块级变量的方法。

具体来说,我编写了一个小型的Python库用于与iTunes通信,并希望拥有一个DOWNLOAD_FOLDER_PATH模块变量。不幸的是,iTunes不会告诉你它的下载文件夹在哪里,因此我编写了一个函数,获取一些播客曲目的文件路径,并向上遍历目录树,直到找到“Downloads”目录。

这需要一两秒钟的时间,因此我希望对其进行延迟评估,而不是在模块导入时评估。

是否有一种方式可以在首次访问模块变量时对其进行延迟分配,还是我必须依赖函数?


3
未来的读者注意:自此问题提出8年后的Python 3.7版本开始,可以使用模块级别的 __getattr__ 实现此功能。 - jedwards
9个回答

61

你无法使用模块实现,但是可以将一个类“伪装成”模块,例如,在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 内更方便您编码为顶级模块对象!_)


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}")

这会抛出 NameError: name 'DOWNLOAD_FOLDER_PATH' is not defined - ChaimG

13

我使用了Alex在Python 3.3上的实现,但是它会严重崩溃:

代码如下:

  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 :-)

如何实现Python中的延迟加载模块变量?

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

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

也许对某些人有用。


10
针对Python 3.5和3.6,根据Python文档,正确的做法是继承types.ModuleType并动态更新模块的__class__。因此,这里提供一个解决方案,参考了Christian Tismer的答案,但可能与其不太相似:
import sys
import types

class _Sneaky(types.ModuleType):
    @property
    def DOWNLOAD_FOLDER_PATH(self):
        if not hasattr(self, '_download_folder_path'):
            self._download_folder_path = '/dev/block/'
        return self._download_folder_path
sys.modules[__name__].__class__ = _Sneaky

对于Python 3.7及以上版本,您可以定义一个模块级别的__getattr__()函数。有关详细信息,请参见PEP 562

1
@AshBerlin-Taylor 你还在使用Python 2.7吗?它将于2020年初停止支持;在它变得不安全之前,你只有9个多月的时间来迁移到其他版本!尤其是不应该在其中编写新软件。如果我提供了这样做的方法,那将是极其不负责任的,我绝对不会以任何形式这样做。请忽略此文本的蓝色,我绝不会以任何形式认可。(https://github.com/wizzwizz4/strictpy/blob/master/strict/utils.py#L26-L50) - wizzwizz4
我希望不是这样,但我们现在仍然需要支持Py.27。 - Ash Berlin-Taylor
请问您能否提供有关此事的Python文档链接? - Spidey
你提到答案基于Python文档:根据Python文档的规范方法来做这件事 - Spidey
这种方法会触发doctest的一个弱点,即只能访问模块上静态定义的属性。https://bugs.python.org/issue46619 - Jason R. Coombs
显示剩余5条评论

7
自Python 3.7起(并作为PEP-562的结果),现在可以通过模块级别的__getattr__实现此功能:
在您的模块内部,添加以下内容:
def _long_function():
    # print() function to show this is called only once
    print("Determining DOWNLOAD_FOLDER_PATH...")
    # Determine the module-level variable
    path = "/some/path/here"
    # Set the global (module scope)
    globals()['DOWNLOAD_FOLDER_PATH'] = path
    # ... and return it
    return path


def __getattr__(name):
    if name == "DOWNLOAD_FOLDER_PATH":
        return _long_function()

    # Implicit else
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

从这里可以清楚地看出,当您导入模块时,_long_function() 不会被执行,例如:
print("-- before import --")
import somemodule
print("-- after import --")

仅需以下结果:

-- 导入前 --
-- 导入后 --

但是,当您试图从模块中访问名称时,将调用模块级别的__getattr__,该方法将调用_long_function,该函数将执行长时间运行的任务,将其缓存为模块级别的变量,并将结果返回给调用它的代码。

例如,使用上述第一个代码块,放在名为“somemodule.py”的模块中,下面的代码:

import somemodule
print("--")
print(somemodule.DOWNLOAD_FOLDER_PATH)
print('--')
print(somemodule.DOWNLOAD_FOLDER_PATH)
print('--')

产生的结果:

--
正在确定DOWNLOAD_FOLDER_PATH...
/some/path/here
--
/some/path/here
--

或者,更明确地说:

# LINE OF CODE                                # OUTPUT
import somemodule                             # (nothing)

print("--")                                   # --

print(somemodule.DOWNLOAD_FOLDER_PATH)        # Determining DOWNLOAD_FOLDER_PATH...
                                              # /some/path/here

print("--")                                   # --

print(somemodule.DOWNLOAD_FOLDER_PATH)        # /some/path/here

print("--")                                   # --

最后,如果您想指示(例如对代码内省工具)DOWNLOAD_FOLDER_PATH是可用的,您也可以按照PEP的描述实现__dir__

4

有没有办法在第一次访问模块变量时进行懒惰赋值,或者我必须依靠函数?

我认为在这里使用函数是解决您问题的最佳方案。 我将给您一个简短的示例来说明。

#myfile.py - an example module with some expensive module level code.

import os
# expensive operation to crawl up in directory structure

如果在模块级别执行,则会执行昂贵的操作。除了懒惰地导入整个模块外,没有方法可以停止这个操作!

#myfile2.py - a module with expensive code placed inside a function.

import os

def getdownloadsfolder(curdir=None):
    """a function that will search upward from the user's current directory
        to find the 'Downloads' folder."""
    # expensive operation now here.

使用这种方法是遵循最佳实践的。

嗯,这是最明显和最简单的方法,符合Python之禅的原则,但就API而言,我不太喜欢它。 - wbg

3

最近我遇到了同样的问题,并找到了解决方法。

class LazyObject(object):
    def __init__(self):
        self.initialized = False
        setattr(self, 'data', None)

    def init(self, *args):
        #print 'initializing'
        pass

    def __len__(self): return len(self.data)
    def __repr__(self): return repr(self.data)

    def __getattribute__(self, key):
        if object.__getattribute__(self, 'initialized') == False:
            object.__getattribute__(self, 'init')(self)
            setattr(self, 'initialized', True)

        if key == 'data':
            return object.__getattribute__(self, 'data')
        else:
            try:
                return object.__getattribute__(self, 'data').__getattribute__(key)
            except AttributeError:
                return super(LazyObject, self).__getattribute__(key)

使用这个LazyObject,您可以为对象定义一个init方法,对象将被懒惰地初始化,示例代码如下:
o = LazyObject()
def slow_init(self):
    time.sleep(1) # simulate slow initialization
    self.data = 'done'
o.init = slow_init

上面的o对象将具有与'done'对象相同的方法,例如,您可以执行以下操作:
# o will be initialized, then apply the `len` method 
assert len(o) == 4

完整的代码和测试(适用于2.7)可以在此处找到:

https://gist.github.com/observerss/007fedc5b74c74f3ea08


1
如果该变量存储在类中而不是模块中,则可以重载getattr,或更好的方法是在init中初始化它。

1
是的,我知道。出于 API 的考虑,我希望将它放在一个模块中。在类中,我会使用 property 来完成它。这对于惰性加载非常好。 - wbg

0

规格 1

懒加载模块属性(和模块)的最知名的方法可能在 scientific-python.org 的 SPEC 1 (草案) 中。SPECs 是科学 Python 生态系统中项目的操作指南。在 Scientific Python Discourse 上讨论了 SPEC 1,并且该解决方案作为 PyPI 中的软件包提供,名称为 lazy_loader。lazy_loader 实现依赖于 Python 3.7 中引入的模块 __gettattr__ 支持(PEP 562),并且它被用于 scikit-imageNetworkXScipy 部分地

使用示例:

以下示例使用lazy_loader PyPI包。您也可以将源代码复制粘贴到您的项目中。

# mypackage/__init__.py
import lazy_loader

__getattr__, __dir__, __all__ = lazy_loader.attach(
    __name__,
    submodules=['bar'],
    submod_attrs={
        'foo.morefoo': ['FooFilter', 'do_foo', 'MODULE_VARIABLE'],
        'grok.spam': ['spam_a', 'spam_b', 'spam_c']
    }
)

这是“懒加载”导入的等效方式

from . import bar
from .foo.morefoo import FooFilter, do_foo, MODULE_VARIABLE
from .grok.spam import (spam_a, spam_b, spam_c)

关于lazy_loader.attach的简短说明

  • 如果您想要懒加载一个模块,请将其列在submodules中(这是一个列表)
  • 如果您想要从模块中懒加载某些内容(函数、类等),请将其列在submod_attrs中(这是一个字典)

类型检查

静态类型检查器和IDE无法从懒加载的导入中推断出类型信息。作为解决方法,您可以使用type stubs(.pyi文件),如下所示:

# mypackage/__init__.pyi
from .foo.morefoo import FooFilter as FooFilter, do_foo as do_foo, MODULE_VARIABLE as MODULE_VARIABLE
from .grok.spam import spam_a as spam_a, spam_b as spam_b, spam_c as spam_c

SPEC 1提到,这种X as X的语法是由于PEP484而必要的

附注

  • 最近有一份懒加载导入的 PEP 提案 PEP 690,但是它被拒绝了。
  • Tensorflow 中有一个懒加载类位于 util.lazyloader
  • Python 核心开发者 Brett Cannon 在2018年发布了一篇博客文章blog post,展示了基于__getattr__的lazy_loader实现,并在一个名为modutil的包中提供。但该项目已被标记为存档状态。这对科学计算 lazy_loader 有很大的启发

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接