Lazy imports in Python have been long discussed and some proposals (for example the PEP609 - Lazy Imports) have been made to make it a built-in (optional) feature in the future.
I am developing a CLI package, so startup time is very important, and I would like to speed it up by lazy loading some of the modules I am using.
What I have so far
By modifying the function to implement lazy imports from Python's importlib documentation, I built the following LazyImport
class:
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
Note: This is the best way I could think of to turn the function to a class, but I'm welcoming feedback on this too if you have a better way.
This works just fine for top-level module imports:
Instead of importing (for example) xarray
as
import xarray as xr
I would run
xr = LazyImport('xarray')
and everything works as expected, with the difference that the xarray
module is added to sys.modules
but it is not loaded in memory yet (the module scripts are not run yet).
The module gets loaded into memory (so the module scripts run) only when the variable xr
is first referenced (for example by calling a method/submodule or simply by referencing it as it is).
So, for the example above, any of these statements would load the xarray
module into memory:
xr.DataArray([1,2,3])
print(xr)
xr
What I want
Now I would like to be able to achieve the same result, but when I load a Class, function or variable from a module.
So (for example) instead of importing the xarray.DataArray
Class through:
from xarray import DataArray as Da
I want to have something like:
Da = LazyImport('DataArray', _from='xarray')
so that the xarray
module is added to sys.modules
but not loaded in memory yet, and will get loaded only when I first reference the Da
variable. The Da
variable will reference the DataArray
Class of the xarray
module.
What I tried
I tried some options such as
xr = LazyImport('xarray')
Da = getattr(xr, 'DataArray')
or by modifying the LazyImport
class, but every time I reference xr
the xarray
module gets loaded in memory. I could not manage to create a Da
variable without loading xarray
in memory.
Referred to the example, what I need is basically a lazy evaluation of the Da
variable that evaluates (to the DataArray
Class of the xarray
module) only when I first reference Da
(and therefore runs the module scripts only at this point).
Also, I don't want any method to be called on the variable Da
to be evaluated (something like Da.load()
for example), but I want the variable to be directly evaluated when first referenced.
I looked at some external libraries (such as lazy_loader), but I haven't found one that allows lazy importing of Classes and variables from external modules (modules other than the one you are developing).
Does anyone know a solution for the implementation of lazy imports from a module?
This answer may not be very satisfying, but I think I've come as close as possible to lazy loading of arbitrary objects as is possible.
Sadly, I think this is as close as you're gonna get to what you want to achieve. We can't use the guts-scooping approach
LazyLoader
uses for non-mutable types. I have another idea I want to pursue, but I'm not confident it's going to lead anywhere.ADDITION
So, turns out you can go further, by writing functions like this:
This gives you a function
replace
which can be used to replace references. Caveats:__dict__
attribute for now (which includes modules and classes).gc.get_referrers
doesn't find local variables.If these caveats are acceptable to you, you can change
LazyAttribute.__getattribute__
to:... and lazily loaded objects will be replaced wherever they can be replaced. And this actually replaces them, not just proxy them or do the guts-scooping
LazyLoader
does.