python 惰性模块变量——可以做到吗?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1462986/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 22:19:22  来源:igfitidea点击:

Lazy module variables--can it be done?

pythonmodulevariableslazy-loadingitunes

提问by wbg

I'm trying to find a way to lazily load a module-level variable.

我试图找到一种方法来延迟加载模块级变量。

Specifically, I've written a tiny Python library to talk to iTunes, and I want to have a DOWNLOAD_FOLDER_PATHmodule variable. Unfortunately, iTunes won't tell you where its download folder is, so I've written a function that grabs the filepath of a few podcast tracks and climbs back up the directory tree until it finds the "Downloads" directory.

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

This takes a second or two, so I'd like to have it evaluated lazily, rather than at module import time.

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

Is there any way to lazily assign a module variable when it's first accessed or will I have to rely on a function?

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

回答by Alex Martelli

You can't do it with modules, but you can disguise a class "as if" it was a module, e.g., in itun.py, code...:

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

Now anybody can import itun... and get in fact your itun._Sneaky()instance. The __getattr__is there to let you access anything else in itun.pythat may be more convenient for you to code as a top-level module object, than inside _Sneaky!_)

现在任何人都可以import itun......并得到你的itun._Sneaky()实例。的__getattr__是有没有让你访问其他任何在itun.py可能对您更方便的代码作为顶层模块对象,比里面_Sneaky!_)

回答by Christian Tismer

I used Alex' implementation on Python 3.3, but this crashes miserably: The code

我在 Python 3.3 上使用了 Alex 的实现,但这次崩溃很惨:代码

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

is not correct because an AttributeErrorshould be raised, not a KeyError. This crashed immediately under Python 3.3, because a lot of introspection is done during the import, looking for attributes like __path__, __loader__etc.

不正确,因为 anAttributeError应该被提出,而不是 a KeyError。这段Python 3.3下立即崩溃,因为很多内省的导入过程完成后,寻找喜欢的属性__path____loader__等等。

Here is the version that we use now in our project to allow for lazy imports in a module. The __init__of the module is delayed until the first attribute access that has not a special name:

这是我们现在在项目中使用的版本,以允许在模块中进行延迟导入。该__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__)

The module now needs to define an initfunction. This function can be used to import modules that might import ourselves:

该模块现在需要定义一个init函数。此函数可用于导入可能导入我们自己的模块:

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

The code can be put into another module, and it can be extended with properties as in the examples above.

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

Maybe that is useful for somebody.

也许这对某人有用。

回答by Dan Walters

It turns out that as of Python 3.7, it's possible to do this cleanly by defining a __getattr__()at the module level, as specified in PEP 562.

事实证明,从 Python 3.7 开始,可以通过__getattr__()在模块级别定义 a 来干净利落地做到这一点,如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}")

回答by jedwards

Since Python 3.7 (and as a result of PEP-562), this is now possible with the module-level __getattr__:

从 Python 3.7(以及PEP-562的结果)开始,现在可以使用模块级别__getattr__

Inside your module, put something like:

在您的模块中,放置如下内容:

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

From this it should be clear that the _long_function()isn't executed when you import your module, e.g.:

从中可以清楚地看出,_long_function()导入模块时不会执行 ,例如:

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

results in just:

结果只是:

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

But when you attempt to access the name fromthe module, the module-level __getattr__will be called, which in turn will call _long_function, which will perform the long-running task, cache it as a module-level variable, and return the result back to the code that called it.

但是当您尝试模块访问名称时,模块级__getattr__将被调用,模块级将调用_long_function,它将执行长时间运行的任务,将其缓存为模块级变量,并将结果返回给调用它的代码。

For example, with the first block above inside the module "somemodule.py", the following code:

例如,对于模块“somemodule.py”中的第一个块,以下代码:

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

produces:

产生:

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

or, more clearly:

或者,更清楚地:

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

Lastly, you can also implement __dir__as the PEP describes if you want to indicate (e.g. to code introspection tools) that DOWNLOAD_FOLDER_PATHisavailable.

最后,您还可以实现__dir__为PEP介绍,如果你想显示(如代码内省工具),这DOWNLOAD_FOLDER_PATH可用的。

回答by Simon Edwards

Is there any way to lazily assign a module variable when it's first accessed or will I have to rely on a function?

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

I think you are correct in saying that a function is the best solution to your problem here. I will give you a brief example to illustrate.

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

#myfile.py - an example module with some expensive module level code.

import os
# expensive operation to crawl up in directory structure

The expensive operation will be executed on import if it is at module level. There is not a way to stop this, short of lazily importing the entire module!!

如果导入是在模块级别,昂贵的操作将在导入时执行。没有办法阻止这种情况,除非懒惰地导入整个模块!!

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

You will be following best practice by using this method.

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

回答by wizzwizz4

The properway of doing this, according to the Python docs, is to subclass types.ModuleTypeand then dynamically update the module's __class__. So, here's a solution loosely on Christian Tismer's answerbut probably not resembling it much at all:

根据 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

回答by jingchao

Recently I came across the same problem, and have found a way to do it.

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

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)

With this LazyObject, You can define a initmethod for the object, and the object will be initialized lazily, example code looks like:

有了这个LazyObject,你可以init为对象定义一个方法,对象会被惰性初始化,示例代码如下:

o = LazyObject()
def slow_init(self):
    time.sleep(1) # simulate slow initialization
    self.data = 'done'
o.init = slow_init

the oobject above will have exactly the same methods whatever 'done'object have, for example, you can do:

o上面的对象将具有与任何'done'对象完全相同的方法,例如,您可以执行以下操作:

# o will be initialized, then apply the `len` method 
assert len(o) == 4

complete code with tests (works in 2.7) can be found here:

可以在此处找到带有测试的完整代码(适用于 2.7):

https://gist.github.com/observerss/007fedc5b74c74f3ea08

https://gist.github.com/observerss/007fedc5b74c74f3ea08

回答by Sean Cavanagh

If that variable lived in a class rather than a module, then you could overload getattr, or better yet, populate it in init.

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