懒惰的模块变量 - 可以做到吗?

时间:2009-09-22 22:32:06

标签: python module variables lazy-loading itunes

我正试图找到一种懒惰加载模块级变量的方法。

具体来说,我写了一个很小的Python库来与iTunes交谈,我希望有一个DOWNLOAD_FOLDER_PATH模块变量。不幸的是,iTunes不会告诉你它的下载文件夹在哪里,所以我写了一个函数来抓取几个播客曲目的文件路径并爬上目录树,直到它找到“下载”目录。

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

有没有办法在第一次访问时懒惰地分配模块变量,还是我必须依赖一个函数?

8 个答案:

答案 0 :(得分:55)

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

答案 1 :(得分:12)

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

Lazy module variables--can it be done?

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

代码可以放入另一个模块,可以使用属性进行扩展 如上例所示。

也许这对某些人有用。

答案 2 :(得分:4)

事实证明,从Python 3.7开始,可以按照PEP 562中的规定,在模块级别定义__getattr__()来干净地做到这一点。

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

答案 3 :(得分:3)

有没有办法在第一次访问时懒惰地分配一个模块变量,还是我必须依赖一个函数?

我认为你说一个函数是解决你问题的最佳方法是正确的。 我将举一个简短的例子来说明。

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

您将通过使用此方法遵循最佳做法。

答案 4 :(得分:2)

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

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

答案 5 :(得分:2)

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

仅得到:

-- before import --
-- after import --

但是,当您尝试从模块中访问名称​​时,模块级__getattr__将被调用,而模块级_long_function将被调用import somemodule print("--") print(somemodule.DOWNLOAD_FOLDER_PATH) print('--') print(somemodule.DOWNLOAD_FOLDER_PATH) print('--') ,该模块将执行长时间运行的任务,将其缓存为模块级变量,然后将结果返回给调用它的代码。

例如,在模块“ somemodule.py”中位于上面的第一个块的情况下,以下代码:

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

产生:

--
Determining DOWNLOAD_FOLDER_PATH...
/some/path/here
--
/some/path/here
--

或更明确地说:

__dir__

最后,您还可以实现PEP所描述的DOWNLOAD_FOLDER_PATH,如果您想表明(例如,对内省工具进行编码)#add port sudo firewall-cmd --add-port=3001/tcp --permanent # add service sudo firewall-cmd --permanent --add-service=http # reload !!! IMPORTANT !!! sudo firewall-cmd --reload 可用。

答案 6 :(得分:0)

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

答案 7 :(得分:0)

Python文档的正确方法是子类types.ModuleType,然后动态更新模块的__class__。因此,这是一个关于Christian Tismer's answer的松散解决方案,但可能与之不太相似:

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