懒惰模块变量 - 能不能做到?

问题描述 投票:30回答:8

我试图找到一种方法,懒洋洋地加载一个模块级变量。

具体来说,我写了一个微小的Python库谈话到iTunes,我想有一个DOWNLOAD_FOLDER_PATH模块变量。不幸的是,iTunes不会告诉你它的下载文件夹,所以我写了,抓住了几个播客轨道的文件路径和爬回了目录树,直到找到“下载”目录中的一个功能。

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

有什么办法懒洋洋地分配一个模块变量时,它的第一次访问,否则我将不得不依赖于一个功能?

python module variables lazy-loading itunes
8个回答
56
投票

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


13
投票

我用的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 :-)

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

现在该模块需要定义一个初始化函数。此功能可用于导入可能导入自己的模块:

def __init__(module):
    ...
    # do something that imports config.py again
    ...

该代码可以被放入另一个模块,并且它可以与性质如在上面的例子中被扩展。

也许这是对别人有用。


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.

你会用这种方法下的最佳实践。


3
投票

事实证明,像Python 3.7的,有可能通过在模块级定义__getattr__()干净做到这一点,如PEP 562规定。

# 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
投票

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

从这一点应该清楚,当您导入模块,e.g不执行_long_function()

print("-- before import --")
import somemodule
print("-- after import --")

结果在短短的:

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

但是当你试图从模块访问的名称,模块级__getattr__将被调用,这反过来将调用_long_function,这将执行长时间运行的任务,缓存它作为一个模块级变量,并返回结果回调用它的代码。

例如,与模块,下面的代码内“somemodule.py”上方的第一个块:

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

生产:

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

最后,还可以实现__dir__作为PEP介绍,如果你想显示(例如编写内省工具),其DOWNLOAD_FOLDER_PATH可用。


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


1
投票

这样做的正确方法,根据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

0
投票

如果该变量住在一类,而不是一个模块,那么你可以重载GETATTR,或者更好的是,在初始化填充它。

© www.soinside.com 2019 - 2024. All rights reserved.