我正在尝试找到一种延迟加载模块级变量的方法。
具体来说,我编写了一个小型 Python 库来与 iTunes 通信,并且我想要一个DOWNLOAD_FOLDER_PATH
模块变量。不幸的是,iTunes 不会告诉你它的下载文件夹在哪里,所以我编写了一个函数来获取一些 podcast 曲目的文件路径并爬回目录树,直到找到“Downloads”目录。
这需要一两秒钟,所以我想让它懒惰地评估,而不是在模块导入时。
有什么方法可以在第一次访问模块变量时懒惰地分配它,还是我必须依赖一个函数?
我正在尝试找到一种延迟加载模块级变量的方法。
具体来说,我编写了一个小型 Python 库来与 iTunes 通信,并且我想要一个DOWNLOAD_FOLDER_PATH
模块变量。不幸的是,iTunes 不会告诉你它的下载文件夹在哪里,所以我编写了一个函数来获取一些 podcast 曲目的文件路径并爬回目录树,直到找到“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__()
在模块级别定义 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}")
我在 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
...
代码可以放入另一个模块中,并且可以使用上面示例中的属性进行扩展。
也许这对某人有用。
根据 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(以及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
可用。
有什么方法可以在第一次访问模块变量时懒惰地分配它,还是我必须依赖一个函数?
我认为您说函数是解决您的问题的最佳方法是正确的。我将举一个简单的例子来说明。
#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):
如果该变量存在于类中而不是模块中,那么您可以重载 getattr,或者更好的是,将其填充到 init 中。