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

wbg*_*wbg 31 python variables module itunes lazy-loading

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

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

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

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

Ale*_*lli 57

你不能用模块来做,但你可以伪装一个类"好像"它是一个模块,例如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()
Run Code Online (Sandbox Code Playgroud)

现在任何人都可以import itun......并且实际上是你的itun._Sneaky()实例.的__getattr__是有没有让你访问其他任何在itun.py可能更方便您作为一个顶级模块目标代码,比里面_Sneaky!_)

  • 这绝对是辉煌的.工作大师. (7认同)

Chr*_*mer 13

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

  def __getattr__(self, name):
    return globals()[name]
Run Code Online (Sandbox Code Playgroud)

是不正确的,因为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 :-)
Run Code Online (Sandbox Code Playgroud)

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

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__)
Run Code Online (Sandbox Code Playgroud)

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

def __init__(module):
    ...
    # do something that imports config.py again
    ...
Run Code Online (Sandbox Code Playgroud)

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

也许那对某人有用.


jed*_*rds 7

从 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}")
Run Code Online (Sandbox Code Playgroud)

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

print("-- before import --")
import somemodule
print("-- after import --")
Run Code Online (Sandbox Code Playgroud)

结果只是:

-- 导入前 --
-- 导入后 --

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

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

import somemodule
print("--")
print(somemodule.DOWNLOAD_FOLDER_PATH)
print('--')
print(somemodule.DOWNLOAD_FOLDER_PATH)
print('--')
Run Code Online (Sandbox Code Playgroud)

产生:

——
正在确定 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("--")                                   # --
Run Code Online (Sandbox Code Playgroud)

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


wiz*_*zz4 6

根据 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
Run Code Online (Sandbox Code Playgroud)


Dan*_*ers 5

事实证明,从Python 3.7开始,可以通过__getattr__()PEP 562中指定的模块级别定义a来完全做到这一点。

# 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}")
Run Code Online (Sandbox Code Playgroud)