从文件位置导入时修补模块属性
Patching module attributes when importing from file location
考虑三个模块 a.py
、b.py
和 main.py
。它们可以各自位于任意位置,但为了示例起见,它们都将位于同一目录中。
模块 a
通过使用 importlib.util.spec_from_file_location
指定其路径来加载 b
,并且 main
对 a
执行相同的操作。
# b.py
eggs = 42
# a.py
import importlib.util
import os
B_PATH = os.path.join(os.path.dirname(__file__), 'b.py') # could be an arbitrary path
spec = importlib.util.spec_from_file_location('b', B_PATH)
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
spam = "ham"
print(f"{__name__} says {b.eggs=}")
# main.py
import importlib.util
import os
A_PATH = os.path.join(os.path.dirname(__file__), 'a.py') # could be an arbitrary path
spec = importlib.util.spec_from_file_location('a', A_PATH)
a = importlib.util.module_from_spec(spec)
spec.loader.exec_module(a)
print(f"{__name__} says {a.spam=}")
当我们运行main.py
:
$ python main.py
a says b.eggs=42
__main__ says a.spam='ham'
如何从 main.py
修补 b.eggs
以便 a.py
看到修补后的值?
从其文件位置加载 b
并在加载 a
之前修补 b.eggs
不起作用:
# main.py v2
import importlib.util
import os
B_PATH = os.path.join(os.path.dirname(__file__), 'b.py')
spec = importlib.util.spec_from_file_location('b', B_PATH)
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
b.eggs = "patched"
A_PATH = os.path.join(os.path.dirname(__file__), 'a.py')
spec = importlib.util.spec_from_file_location('a', A_PATH)
a = importlib.util.module_from_spec(spec)
spec.loader.exec_module(a)
print(f"{__name__} says {a.spam=}")
print(f"{__name__} says {b.eggs=}")
$ python main.py
a says b.eggs=42
__main__ says a.spam='ham'
__main__ says b.eggs='patched'
如何在不修补 b
的实际源代码的情况下执行此操作?
(请注意,在这个例子中,因为所有模块都在同一个目录中,所以从文件路径导入是愚蠢的,因为它可以用 import
语句完成,但这只是为了最小的工作示例。在我的真实情况下,我的模块位于不同的目录中,我无法修改 PYTHONPATH
环境变量,所以我这样做。)
如图所示从源导入模块不会检查或更新 sys.modules
,因为它不使用 __import__
. The Language Reference says:
A direct call to __import__()
[...]
certain side-effects may occur, such as the importing of parent packages, and the updating of various caches (including sys.modules), [...]
因此,为了让a
和main
看到相同的模块对象,我们需要手动检查和更新sys.modules
。因此,要从它们的路径加载模块,我们可以使用这个函数:
import sys, types
def load_from_path(name: str, path: str) -> types.ModuleType:
if name not in sys.modules:
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
sys.modules[name] = mod
return sys.modules[name]
因此上面的例子变成了(假设load_from_path
在所有模块中定义):
# b.py
eggs = 42
# a.py
import importlib.util
import os
B_PATH = os.path.join(os.path.dirname(__file__), 'b.py')
b = load_from_path('b', B_PATH)
spam = "ham"
print(f"{__name__} says {b.eggs=}")
# main.py
import importlib.util
import os
A_PATH = os.path.join(os.path.dirname(__file__), 'a.py')
B_PATH = os.path.join(os.path.dirname(__file__), 'b.py')
b = load_from_path('b', B_PATH)
b.eggs = "patched"
a = load_from_path('a', A_PATH)
print(f"{__name__} says {a.spam=}")
print(f"{__name__} says {b.eggs=}")
输出为:
a says b.eggs='patched'
__main__ says a.spam='ham'
__main__ says b.eggs='patched'
考虑三个模块 a.py
、b.py
和 main.py
。它们可以各自位于任意位置,但为了示例起见,它们都将位于同一目录中。
模块 a
通过使用 importlib.util.spec_from_file_location
指定其路径来加载 b
,并且 main
对 a
执行相同的操作。
# b.py
eggs = 42
# a.py
import importlib.util
import os
B_PATH = os.path.join(os.path.dirname(__file__), 'b.py') # could be an arbitrary path
spec = importlib.util.spec_from_file_location('b', B_PATH)
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
spam = "ham"
print(f"{__name__} says {b.eggs=}")
# main.py
import importlib.util
import os
A_PATH = os.path.join(os.path.dirname(__file__), 'a.py') # could be an arbitrary path
spec = importlib.util.spec_from_file_location('a', A_PATH)
a = importlib.util.module_from_spec(spec)
spec.loader.exec_module(a)
print(f"{__name__} says {a.spam=}")
当我们运行main.py
:
$ python main.py
a says b.eggs=42
__main__ says a.spam='ham'
如何从 main.py
修补 b.eggs
以便 a.py
看到修补后的值?
从其文件位置加载 b
并在加载 a
之前修补 b.eggs
不起作用:
# main.py v2
import importlib.util
import os
B_PATH = os.path.join(os.path.dirname(__file__), 'b.py')
spec = importlib.util.spec_from_file_location('b', B_PATH)
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
b.eggs = "patched"
A_PATH = os.path.join(os.path.dirname(__file__), 'a.py')
spec = importlib.util.spec_from_file_location('a', A_PATH)
a = importlib.util.module_from_spec(spec)
spec.loader.exec_module(a)
print(f"{__name__} says {a.spam=}")
print(f"{__name__} says {b.eggs=}")
$ python main.py
a says b.eggs=42
__main__ says a.spam='ham'
__main__ says b.eggs='patched'
如何在不修补 b
的实际源代码的情况下执行此操作?
(请注意,在这个例子中,因为所有模块都在同一个目录中,所以从文件路径导入是愚蠢的,因为它可以用 import
语句完成,但这只是为了最小的工作示例。在我的真实情况下,我的模块位于不同的目录中,我无法修改 PYTHONPATH
环境变量,所以我这样做。)
如图所示从源导入模块不会检查或更新 sys.modules
,因为它不使用 __import__
. The Language Reference says:
A direct call to
__import__()
[...] certain side-effects may occur, such as the importing of parent packages, and the updating of various caches (including sys.modules), [...]
因此,为了让a
和main
看到相同的模块对象,我们需要手动检查和更新sys.modules
。因此,要从它们的路径加载模块,我们可以使用这个函数:
import sys, types
def load_from_path(name: str, path: str) -> types.ModuleType:
if name not in sys.modules:
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
sys.modules[name] = mod
return sys.modules[name]
因此上面的例子变成了(假设load_from_path
在所有模块中定义):
# b.py
eggs = 42
# a.py
import importlib.util
import os
B_PATH = os.path.join(os.path.dirname(__file__), 'b.py')
b = load_from_path('b', B_PATH)
spam = "ham"
print(f"{__name__} says {b.eggs=}")
# main.py
import importlib.util
import os
A_PATH = os.path.join(os.path.dirname(__file__), 'a.py')
B_PATH = os.path.join(os.path.dirname(__file__), 'b.py')
b = load_from_path('b', B_PATH)
b.eggs = "patched"
a = load_from_path('a', A_PATH)
print(f"{__name__} says {a.spam=}")
print(f"{__name__} says {b.eggs=}")
输出为:
a says b.eggs='patched'
__main__ says a.spam='ham'
__main__ says b.eggs='patched'