【问题标题】:Patching module attributes when importing from file location从文件位置导入时修补模块属性
【发布时间】:2020-05-28 10:48:17
【问题描述】:

考虑三个模块a.pyb.pymain.py。它们都可以位于任意位置,但为了示例,它们都将位于同一目录中。

模块a 通过使用importlib.util.spec_from_file_location 指定其路径来加载bmaina 执行相同的操作。

# 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 环境变量,所以我这样做。)

【问题讨论】:

    标签: python import python-import monkeypatching


    【解决方案1】:

    如图所示从源导入模块不会检查或更新sys.modules,因为它不使用__import__Language Reference says

    直接致电__import__() [...] 可能会出现某些副作用,例如导入父包,更新各种缓存(包括 sys.modules),[...]

    因此,amain 要看到相同的模块对象,我们需要手动检查和更新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'
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-02-22
      • 2019-12-09
      • 2021-08-31
      • 1970-01-01
      • 1970-01-01
      • 2018-05-16
      • 2018-02-26
      相关资源
      最近更新 更多