Importe dinámicamente el módulo desde la memoria en Python 3 usando Hooks
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
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 defind_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)