Importe dinámicamente el módulo desde la memoria en Python 3 usando Hooks

Nov 25 2020

Lo que quiero lograr es exactamente lo que propone esta respuesta , sin embargo, en Python 3.

El siguiente código funciona bien en 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

Sin embargo, al realizar los cambios obvios en Python 3 (adjuntando exec e print entre paréntesis) obtengo el siguiente código:

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

No es que el exec()cambio fuera muy significativo. No entendí lo que hizo esa línea en Python 2, la "traduje" de la manera que creo que es correcta. Sin embargo, el código de Python 3 me da el siguiente error:

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'

¿Qué debo cambiar en el código para que funcione en Python 3 exactamente de la misma manera que funciona en Python 2?

Observación: Esto no responde a mi pregunta ya que no estoy interesado en importar un módulo de .pyc.

Respuestas

1 ZachGates Nov 27 2020 at 08:28

La respuesta corta es que olvidó traducir la segunda mitad de la execdeclaración del código de muestra. Eso hace execque se aplique inel contexto del load_modulemétodo, no el new_module; así que especifica el contexto:

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

Sin embargo, si usa una versión de Python 3.4 o superior, estará sujeto a PEP 451 (la introducción de especificaciones del módulo ), así como a la desaprobación del impmódulo, a favor de importlib. Particularmente:

  • La imp.new_module(name)función se reemplaza por importlib.util.module_from_spec(spec).
  • Una clase base abstracta para objetos Path Finder meta es suministrado: importlib.abc.MetaPathFinder.
  • Y esos objetos de búsqueda ahora usan en find_speclugar de find_module.

Aquí hay una reimplementación muy cercana del ejemplo de código.

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)