使用 Hooks 从 Python 3 中的内存动态导入模块

Dynamically import module from memory in Python 3 using Hooks

我想要实现的正是这个 this answer 提出的,但是在 Python 3.

下面的代码在 Python 2:

中运行良好
import sys
import imp

modules = {
"my_module":
"""class Test:
    def __init__(self):
        self.x = 5
    def print_number(self):
        print self.x"""}    

class StringImporter(object):

   def __init__(self, modules):
       self._modules = dict(modules)


   def find_module(self, fullname, path):
      if fullname in self._modules.keys():
         return self
      return None

   def load_module(self, fullname):
      if not fullname in self._modules.keys():
         raise ImportError(fullname)

      new_module = imp.new_module(fullname)
      exec self._modules[fullname] in new_module.__dict__
      return new_module


if __name__ == '__main__':
   sys.meta_path.append(StringImporter(modules))

   from my_module import Test
   my_test = Test()
   my_test.print_number() # prints 5

但是,当对 Python 3 进行明显更改时(将 exec 和 print 括在括号中)我得到以下代码:

import sys
import imp

modules = {
"my_module":
"""class Test:
    def __init__(self):
        self.x = 5
    def print_number(self):
        print(self.x)"""}    

class StringImporter(object):

   def __init__(self, modules):
       self._modules = dict(modules)


   def find_module(self, fullname, path):
      if fullname in self._modules.keys():
         return self
      return None

   def load_module(self, fullname):
      if not fullname in self._modules.keys():
         raise ImportError(fullname)

      new_module = imp.new_module(fullname)
      exec(self._modules[fullname])
      return new_module


if __name__ == '__main__':
   sys.meta_path.append(StringImporter(modules))

   from my_module import Test
   my_test = Test()
   my_test.print_number() # Should print 5

并不是说 exec() 的变化非常显着。我不明白那行在 Python 2 中做了什么,我按照我认为正确的方式“翻译”了它。但是,Python 3 代码给我以下错误:

Traceback (most recent call last):
  File "main.py", line 35, in <module>
    from my_module import Test
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 655, in _load_unlocked
  File "<frozen importlib._bootstrap>", line 626, in _load_backward_compatible
KeyError: 'my_module'

为了在 Python 3 中的工作方式与在 Python 2 中的工作方式完全相同,我应该对代码进行哪些更改?

观察:This 没有回答我的问题,因为我对从 .pyc.

导入模块不感兴趣

简短的回答是您忘记翻译代码示例中 exec 语句的后半部分。这导致 exec 被应用 in load_module 方法的上下文——而不是 new_module;所以指定上下文:

exec(self._modules[fullname], new_module.__dict__)

但是,使用 Python 3.4 或更高版本,您将受到 PEP 451 (the introduction of module specs), as well as the deprecation of the imp module, in favor of importlib 的约束。特别是:

  • imp.new_module(name) 函数被 importlib.util.module_from_spec(spec) 取代。
  • 提供了元路径查找器对象的抽象基础 class:importlib.abc.MetaPathFinder
  • 并且此类查找器对象现在使用 find_spec 而不是 find_module

这里是代码示例的非常接近的重新实现。

import importlib
import sys
import types


class StringLoader(importlib.abc.Loader):

    def __init__(self, modules):
        self._modules = modules

    def has_module(self, fullname):
        return (fullname in self._modules)

    def create_module(self, spec):
        if self.has_module(spec.name):
            module = types.ModuleType(spec.name)
            exec(self._modules[spec.name], module.__dict__)
            return module

    def exec_module(self, module):
        pass


class StringFinder(importlib.abc.MetaPathFinder):

    def __init__(self, loader):
        self._loader = loader

    def find_spec(self, fullname, path, target=None):
        if self._loader.has_module(fullname):
            return importlib.machinery.ModuleSpec(fullname, self._loader)


if __name__ == '__main__':
    modules = {
        'my_module': """
    BAZ = 42

    class Foo:
        def __init__(self, *args: str):
            self.args = args
        def bar(self):
            return ', '.join(self.args)
    """}

    finder = StringFinder(StringLoader(modules))
    sys.meta_path.append(finder)

    import my_module
    foo = my_module.Foo('Hello', 'World!')
    print(foo.bar())
    print(my_module.BAZ)