Last active
April 12, 2025 00:49
-
-
Save mara004/6915e904797916b961e9c53b4fc874ec to your computer and use it in GitHub Desktop.
Various attempts at deferred ("lazy") imports. None of these seems particularly satisfying, though. Missing PEP 690...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# SPDX-FileCopyrightText: 2024 geisserml <[email protected]> | |
# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause OR MPL-2.0 | |
import sys | |
import importlib.util | |
def v1_deferred_import(modpath): | |
# FIXME If modpath points to a submodule (e.g. PIL.Image), the parent module will be loaded immediately when this function is called. What's more, non-deferred imports of the submodule will break. This seems to be a nasty limitation of the importlib APIs used here. | |
module = sys.modules.get(modpath, None) | |
if module is not None: | |
return module # shortcut | |
# assuming an optional dependency | |
# returning None will simply let it fail with an AttributeError when attempting to access the module | |
try: | |
spec = importlib.util.find_spec(modpath) | |
except ModuleNotFoundError: | |
return None | |
if spec is None: | |
return None | |
# see https://docs.python.org/3/library/importlib.html#implementing-lazy-imports | |
loader = importlib.util.LazyLoader(spec.loader) | |
spec.loader = loader | |
module = importlib.util.module_from_spec(spec) | |
sys.modules[modpath] = module | |
loader.exec_module(module) | |
return module | |
# --------------- | |
import importlib | |
import functools | |
if sys.version_info < (3, 8): | |
# NOTE alternatively, we could write our own cached property backport with python's descriptor protocol | |
def cached_property(func): | |
return property( functools.lru_cache(maxsize=1)(func) ) | |
else: | |
cached_property = functools.cached_property | |
class v2_DeferredModule: | |
# NOTE Attribute assigment will affect only the wrapper, not the actual module. | |
# However, you can add a __setattr__ to the class once all instance objects have been initialized. | |
# This avoids distasteful attribute checks in __setattr__. | |
def __init__(self, modpath): | |
self._modpath = modpath | |
def __repr__(self): | |
return f"<deferred module wrapper {self._modpath!r}>" | |
@cached_property | |
def _module(self): | |
# print("actually importing module...") | |
return importlib.import_module(self._modpath) | |
def __getattr__(self, k): | |
return getattr(self._module, k) | |
# --------------- | |
import importlib | |
class v3_ModulePlaceholder: | |
# NOTE Instances of this class are bound to a single namespace, so `from nsp import MyModule` would cause breakage. However, `nsp.MyModule` should work. | |
def __init__(self, modpath, nsp): # pass nsp=globals() | |
self._modpath = modpath | |
self._nsp = nsp | |
self._count = 0 | |
def __repr__(self): | |
return f"<deferred module placeholder {self._modpath!r}>" | |
def __getattr__(self, k): | |
assert self._count == 0, "Placeholder must be replaced by actual module on first attribute access." | |
self._count += 1 | |
name = self._modpath.replace(".", "_") | |
module = importlib.import_module(self._modpath) | |
self._nsp[name] = module | |
return getattr(module, k) | |
# --------------- | |
import importlib | |
import lazy_object_proxy # third-party | |
def v4_deferred_import(modpath): | |
return lazy_object_proxy.Proxy(lambda: importlib.import_module(modpath)) |
Yet another approach:
import sys
if sys.version_info < (3, 8):
from functools import lru_cache
def cached_property(func):
return property( lru_cache(maxsize=1)(func) )
else:
from functools import cached_property
class _LazyClass:
@cached_property
def numpy(self):
print("Evaluating lazy import 'numpy' ...", file=sys.stderr)
import numpy; return numpy
@cached_property
def PIL_Image(self):
print("Evaluating lazy import 'PIL.Image' ...", file=sys.stderr)
import PIL.Image; return PIL.Image
Lazy = _LazyClass()
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
As to
v1_deferred_imports()
, the culprit for the immediate loading of parent modules seems to beimportlib.util.find_spec()
:I wonder if we could by any chance replace this with a direct instantiation of
importlib.machinery.ModuleSpec
or something to bypass this limitation.