代码之家  ›  专栏  ›  技术社区  ›  wbg

懒惰的模块变量——能做到吗?

  •  30
  • wbg  · 技术社区  · 15 年前

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

    具体来说,我写了一个很小的python库来与iTunes对话,我想有一个 DOWNLOAD_FOLDER_PATH 模块变量。不幸的是,iTunes不会告诉你它的下载文件夹在哪里,所以我编写了一个函数,它可以抓取一些播客曲目的文件路径,然后向目录树上爬,直到找到“downloads”目录。

    这需要一两秒钟的时间,所以我想让它的评估比较慢,而不是在模块导入时。

    在第一次访问模块变量时,是否有任何方法可以惰性地分配它,或者我必须依赖一个函数?

    8 回复  |  直到 6 年前
        1
  •  54
  •   Alex Martelli    15 年前

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

        2
  •  12
  •   Community CDub    8 年前

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

    模块现在需要定义 初始化 功能。此功能可以使用 要导入可能导入我们自己的模块,请执行以下操作:

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

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

    也许这对某人有用。

        3
  •  4
  •   Dan Walters    6 年前

    事实证明,从Python3.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}")
    
        4
  •  3
  •   Simon Edwards    15 年前

    在第一次访问模块变量时,是否有任何方法可以惰性地分配它,或者我必须依赖一个函数?

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

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

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

        5
  •  2
  •   jingchao    12 年前

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

    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
  •  0
  •   Sean Cavanagh    15 年前

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

        7
  •  0
  •   wizzwizz4    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
    
        8
  •  0
  •   jedwards    6 年前

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

    但是当你试图访问这个名字时 模块,模块级别 阿-格二氏 将被调用,然后调用 _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__ 正如政治公众人物所描述的,如果你想表明(例如编码自省工具),那么 DOWNLOAD_FOLDER_PATH 可用。