python-3.x 从模块延迟导入(也称为(lazy evaluation)

n6lpvg4x  于 2023-10-21  发布在  Python
关注(0)|答案(2)|浏览(122)

Python中的惰性导入已经讨论了很长时间,并且已经提出了一些建议(例如PEP609 - Lazy Imports),使其成为未来的内置(可选)功能。
我正在开发一个CLI包,所以启动时间非常重要,我想通过延迟加载我正在使用的一些模块来加快它。

到目前为止

通过修改函数以实现从Python的importlib documentation的延迟导入,我构建了以下LazyImport类:

import importlib.util
import sys
from types import ModuleType

class LazyImport:
    def __init__(self):
        pass

    def __new__(
            cls,
            name: str,
    ) -> type(ModuleType):
        try:
            return sys.modules[name]
        except KeyError:
            spec = importlib.util.find_spec(name)
            if spec:
                loader = importlib.util.LazyLoader(spec.loader)
                spec.loader = loader
                module = importlib.util.module_from_spec(spec)
                sys.modules[name] = module
                loader.exec_module(module)
                return module
            else:
                raise ModuleNotFoundError(f"No module named '{name}'") from None
  • 注:* 这是我能想到的将函数转换为类的最佳方法,但如果你有更好的方法,我也欢迎你对此提出反馈。

这对于顶级模块导入来说很好:
而不是导入(例如)xarray作为

import xarray as xr

我会跑

xr = LazyImport('xarray')

一切都按预期工作,不同的是xarray模块被添加到sys.modules,但它还没有加载到内存中(模块脚本还没有运行)。
只有当变量xr第一次被引用时(例如通过调用方法/子模块或简单地引用它),模块才会被加载到内存中(因此模块脚本运行)。因此,对于上面的示例,这些语句中的任何一个都将把xarray模块加载到内存中:

  • xr.DataArray([1,2,3])
  • print(xr)
  • xr
    我想要的

现在,我希望能够实现相同的结果,但当我从模块中加载类,函数或变量时。
因此(例如)不是通过以下方式导入xarray.DataArray类:

from xarray import DataArray as Da

我想要的是:

Da = LazyImport('DataArray', _from='xarray')

这样xarray模块就被添加到sys.modules中,但还没有加载到内存中,只有当我第一次引用Da变量时才会被加载。Da变量将引用xarray模块的DataArray类。

我努力

我尝试了一些选项,例如

xr = LazyImport('xarray')
Da = getattr(xr, 'DataArray')

或者通过修改LazyImport类,但是每次我引用xr时,xarray模块都会加载到内存中。我无法在内存中不加载xarray的情况下创建Da变量。
参考示例,我需要的基本上是Da变量的延迟求值,仅当我第一次引用Da时才求值(xarray模块的DataArray类)(因此仅在此时运行模块脚本)。
另外,我不希望在要计算的变量Da上调用任何方法(例如Da.load()),但我希望在第一次引用时直接计算变量。
我查看了一些外部库(比如lazy_loader),但没有发现一个允许从外部模块(除了您正在开发的模块)延迟导入类和变量的库。
有谁知道从模块中实现延迟导入的解决方案吗?

3b6akqbq

3b6akqbq1#

你可以让你的lazy对象充当.getattr)和()call)操作的代理:

class Lazy:
    def __init__(self, mod, name):
        self.mod = mod
        self.name = name

    def __getattr__(self, item):
        return getattr(self._target(), item)

    def __call__(self, *args, **kwargs):
        return self._target()(*args, **kwargs)

    def _target(self):
        if self.mod not in sys.modules:
            __import__(self.mod)
        return getattr(sys.modules[self.mod], self.name)

r = Lazy('random', 'randint')
print(r(1, 45))

C = Lazy('collections', 'Counter')
print(C([1, 2, 1]))

但这是相当脆弱的-如果有一天你决定懒惰地导入一个常数呢?还是传递“导入”变量?您最初的方法要好得多,只需坚持使用xr.DataArray即可。

9avjhtql

9avjhtql2#

这个答案可能不是很令人满意,但我认为我已经尽可能接近于任意对象的延迟加载。

import importlib.util
import sys
from types import ModuleType
from inspect import getattr_static
from typing import Any
from collections.abc import Callable

def get_real_object(lazy_attribute: 'LazyAttribute') -> Any:
    obj = getattr_static(lazy_attribute, 'obj')
    attr = getattr_static(lazy_attribute, 'attr')
    return getattr(obj, attr)

class LazyAttribute:
    def __init__(self, obj: ModuleType, attr: str) -> None:
        self.obj = obj
        self.attr = attr
    def __getattribute__(self, attr: str) -> Any:
        return getattr(get_real_object(self), attr)

def getmethod(name: str) -> Callable:
    def method(self, *args: Any, **kwargs: Any) -> Any:
        real_object = get_real_object(self)
        return getattr(type(real_object), name)(real_object, *args, **kwargs)
    method.__name__ = name
    return method

# proxied magic methods
# Python does not use its dynamic lookup mechanisms for those, so they will
# really need to be set on the LazyAttribute class
# I just did a few, you can add what you need. You can add the whole object model if you like (except __getattribute__ and __init__).
for name in ['__call__', '__lt__', '__eq__', '__repr__', '__str__', '__gt__']:
    setattr(LazyAttribute, name, getmethod(name))

def lazy_import(name: str) -> ModuleType:
    try:
        return sys.modules[name]
    except KeyError:
        spec = importlib.util.find_spec(name)
        if spec:
            loader = importlib.util.LazyLoader(spec.loader)
            spec.loader = loader
            module = importlib.util.module_from_spec(spec)
            sys.modules[name] = module
            loader.exec_module(module)
            return module
        else:
            raise ModuleNotFoundError(f"No module named '{name}'") from None

def lazy_from_import(module: str, name: str) -> LazyAttribute:
    return LazyAttribute(lazy_import(module), name)

########
# Example:

# in testmod.py:
#   one = 1
#   def two():
#       print('hello')
#   class Three:
#       pass
one = lazy_from_import('testmod', 'one')
two = lazy_from_import('testmod', 'two')
Three = lazy_from_import('testmod', 'Three')

print(one)
two()
print(Three())

# Limitations: they're not the real deal, just proxies
try:
    print(one > one)
except TypeError as e:
    print(e)
try:
    print(5 * one)
except TypeError as e:
    print(e)

import testmod # get access to the real objects

assert one is not testmod.one
assert Three is not testmod.Three
assert type(one) is not type(testmod.one)

可悲的是,我想这是你最接近你想要达到的目标了。我们不能使用LazyLoader对不可变类型使用的guts-scooping方法。我有另一个想法,我想追求,但我不相信它会导致任何地方。

添加

所以,事实证明你可以更进一步,通过编写这样的函数:

import gc

def replace_in(ref, obj, by):
    if isinstance(ref, list):
        for i, item in enumerate(ref):
            if item is obj:
                ref[i] = by
    elif isinstance(ref, dict):
        for key, val in ref.items():
            if val is obj:
                ref[key] = by
        if obj in ref:
            ref[by] = ref.pop(obj)
    elif isinstance(ref, tuple):
        raise TypeError('tuples are immutable, consider storing lazy values in a list instead -- or make sure to evaluate the value before constructing this tuple')
    elif hasattr(ref, '__dict__'):
        ref = ref.__dict__
        for key, val in ref.items():
            if val is obj:
                ref[key] = by
    else:
        raise TypeError(f'cannot replace {type(ref)} object yet')

def replace(obj, by):
    for ref in gc.get_referrers(obj):
        replace_in(ref, obj, by)

这给了你一个函数replace,它可以用来替换引用。注意事项:
1.这使用了一个用于调试的函数。来自文档:

警告:使用get_referrers()返回的对象时必须小心,因为其中一些对象可能仍在构造中,因此处于暂时无效的状态。避免将get_referrers()用于除调试以外的任何目的。

1.它目前只支持列表、字典和带有__dict__属性的对象(包括模块和类)。
1.由于显而易见的原因,直接存储在元组和其他不可变类中的值不能被替换。

  1. gc.get_referrers找不到局部变量。
    1.当替换字典中用作键的对象时,它会改变顺序。这可以以降低运行时性能为代价来修复(将替换键之后的所有项移到后面)。
    如果您可以接受这些警告,则可以将LazyAttribute.__getattribute__更改为:
def __getattribute__(self, attr):
    real = get_real_object(self)
    replace(self, real)
    return getattr(real, attr)

.并且延迟加载的对象将在任何可以替换的地方被替换。这实际上取代了它们,而不仅仅是代理它们或做LazyLoader做的事情。

相关问题