【问题标题】:Lazy module variables--can it be done?懒惰的模块变量——可以吗?
【发布时间】:2010-11-30 14:00:36
【问题描述】:

我正在尝试找到一种延迟加载模块级变量的方法。

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

这需要一两秒钟,所以我希望延迟评估它,而不是在模块导入时。

有什么方法可以在第一次访问模块变量时懒惰地分配它,还是我必须依赖一个函数?

【问题讨论】:

  • 给未来读者的注意事项:自 Python 3.7 以来(被问到此问题 8 年后)this is now possible 与模块级别 __getattr__

标签: python module variables lazy-loading itunes


【解决方案1】:

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

【讨论】:

  • 是的,我知道。为了 API,我希望它在一个模块中。不过,在课堂上,我会使用property 来做这件事。非常适合延迟加载。
【解决方案2】:

有什么方法可以在第一次访问模块变量时延迟分配它,还是我必须依赖一个函数?

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

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

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

【讨论】:

  • 嗯。这是显而易见且最简单的方法,因此符合 Python 的禅宗,但我只是不喜欢它的 API。
【解决方案3】:

你不能用模块来做到这一点,但你可以把一个类伪装成“好像”它是一个模块,例如,在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 中更方便您将代码编写为顶级模块对象!_)

【讨论】:

    【解决方案4】:

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

    代码可以放到另一个模块中,可以用属性扩展 和上面的例子一样。

    也许这对某人有用。

    【讨论】:

      【解决方案5】:

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

      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

      【讨论】:

        【解决方案6】:

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

        【讨论】:

        【解决方案7】:

        事实证明,从 Python 3.7 开始,可以通过在模块级别定义 __getattr__() 来干净地执行此操作,如 PEP 562 中指定的那样,并在 Python 参考文档中的 data model chapter 中记录。

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

        【讨论】:

          【解决方案8】:

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

          最后,您还可以按照 PEP 的描述实现 __dir__,如果您想指示(例如,编码自省工具)DOWNLOAD_FOLDER_PATH 可用。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-11-25
            • 1970-01-01
            • 2018-08-15
            • 2019-12-28
            • 2018-09-01
            • 1970-01-01
            • 2018-01-28
            相关资源
            最近更新 更多