我正在尝试找到一种延迟加载模块级变量的方法。
具体来说,我编写了一个小型的Python库用于与iTunes通信,并希望拥有一个DOWNLOAD_FOLDER_PATH
模块变量。不幸的是,iTunes不会告诉你它的下载文件夹在哪里,因此我编写了一个函数,获取一些播客曲目的文件路径,并向上遍历目录树,直到找到“Downloads”目录。
这需要一两秒钟的时间,因此我希望对其进行延迟评估,而不是在模块导入时评估。
是否有一种方式可以在首次访问模块变量时对其进行延迟分配,还是我必须依赖函数?
我正在尝试找到一种延迟加载模块级变量的方法。
具体来说,我编写了一个小型的Python库用于与iTunes通信,并希望拥有一个DOWNLOAD_FOLDER_PATH
模块变量。不幸的是,iTunes不会告诉你它的下载文件夹在哪里,因此我编写了一个函数,获取一些播客曲目的文件路径,并向上遍历目录树,直到找到“Downloads”目录。
这需要一两秒钟的时间,因此我希望对其进行延迟评估,而不是在模块导入时评估。
是否有一种方式可以在首次访问模块变量时对其进行延迟分配,还是我必须依赖函数?
你无法使用模块实现,但是可以将一个类“伪装成”模块,例如,在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
内更方便您编码为顶级模块对象!_)
原来在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我使用了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 :-)
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
...
这段代码可以放到另一个模块中,并且像上面的例子那样可以使用属性进行扩展。
也许对某些人有用。
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
__getattr__()
函数。有关详细信息,请参见PEP 562。根据Python文档的规范方法来做这件事
。 - Spidey__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__
。有没有办法在第一次访问模块变量时进行懒惰赋值,或者我必须依靠函数?
我认为在这里使用函数是解决您问题的最佳方案。 我将给您一个简短的示例来说明。
#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.
最近我遇到了同样的问题,并找到了解决方法。
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)可以在此处找到:
property
来完成它。这对于惰性加载非常好。 - wbg懒加载模块属性(和模块)的最知名的方法可能在 scientific-python.org 的 SPEC 1 (草案) 中。SPECs 是科学 Python 生态系统中项目的操作指南。在 Scientific Python Discourse 上讨论了 SPEC 1,并且该解决方案作为 PyPI 中的软件包提供,名称为 lazy_loader。lazy_loader 实现依赖于 Python 3.7 中引入的模块 __gettattr__
支持(PEP 562),并且它被用于 scikit-image、NetworkX 和 Scipy 部分地。
以下示例使用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)
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而必要的。
__getattr__
的lazy_loader实现,并在一个名为modutil的包中提供。但该项目已被标记为存档状态。这对科学计算 lazy_loader 有很大的启发。
__getattr__
实现此功能。 - jedwards