41

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

具体来说,我编写了一个小型 Python 库来与 iTunes 通信,并且我想要一个DOWNLOAD_FOLDER_PATH模块变量。不幸的是,iTunes 不会告诉你它的下载文件夹在哪里,所以我编写了一个函数来获取一些 podcast 曲目的文件路径并爬回目录树,直到找到“Downloads”目录。

这需要一两秒钟,所以我想让它懒惰地评估,而不是在模块导入时。

有什么方法可以在第一次访问模块变量时懒惰地分配它,还是我必须依赖一个函数?

4

8 回答 8

60

你不能用模块来做,但你可以伪装一个类“好像”它是一个模块,例如,在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!_)

于 2009-09-23T03:09:28.000 回答
17

事实证明,从 Python 3.7 开始,可以通过__getattr__()在模块级别定义 a 来干净地做到这一点,如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}")
于 2018-09-16T23:25:19.133 回答
13

我在 Python 3.3 上使用了 Alex 的实现,但这很糟糕:代码

  def __getattr__(self, name):
    return globals()[name]

不正确,因为AttributeError应该提出 an ,而不是 a 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
    ...

代码可以放入另一个模块中,并且可以使用上面示例中的属性进行扩展。

也许这对某人有用。

于 2013-01-15T19:57:57.530 回答
9

根据 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
于 2018-08-25T15:26:07.117 回答
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...
/一些/路径/这里
--
/一些/路径/这里
--

或者,更清楚地说:

# 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("--")                                   # --

最后,您还可以__dir__按照 PEP 的描述来实现是否要指示(例如,编码自省工具)DOWNLOAD_FOLDER_PATH 可用

于 2019-02-10T12:52:15.190 回答
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.

您将使用此方法遵循最佳实践。

于 2009-09-22T23:10:16.417 回答
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

于 2013-06-05T10:04:07.383 回答
1

如果该变量存在于类中而不是模块中,那么您可以重载 getattr,或者更好的是,将其填充到 init 中。

于 2009-09-22T22:44:47.707 回答