Importa dinamicamente il modulo dalla memoria in Python 3 utilizzando Hooks
Quello che voglio ottenere è esattamente ciò che questa risposta propone, tuttavia in Python 3.
Il codice seguente funziona bene in 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
Tuttavia, quando apporto le ovvie modifiche a Python 3 (racchiudendo exec e print tra parentesi) ottengo il seguente codice:
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
Non che il exec()cambiamento sia stato piuttosto significativo. Non ho capito cosa facesse quella riga in Python 2, l'ho "tradotta" nel modo in cui penso sia corretta. Tuttavia, il codice Python 3 mi dà il seguente errore:
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'
Cosa dovrei cambiare nel codice per funzionare in Python 3 esattamente nello stesso modo in cui funziona in Python 2?
Osservazione: questo non risponde alla mia domanda in quanto non sono interessato a importare un modulo da .pyc.
Risposte
La risposta breve è che hai dimenticato di tradurre l'ultima metà execdell'istruzione dall'esempio di codice. Ciò fa sì execche venga applicato inil contesto del load_modulemetodo, non il new_module; quindi specifica il contesto:
exec(self._modules[fullname], new_module.__dict__)
Tuttavia, utilizzando una versione di Python 3.4 o successiva, diventi soggetto a PEP 451 (l'introduzione delle specifiche del modulo ), nonché alla deprecazione del impmodulo, a favore di importlib. In particolar modo:
- La imp.new_module(name)funzione è sostituita da
importlib.util.module_from_spec(spec). - Una classe base astratta per gli oggetti finder meta percorso è fornito:
importlib.abc.MetaPathFinder. - E tali oggetti finder ora usano
find_specinvece difind_module.
Ecco una reimplementazione molto ravvicinata del codice di esempio.
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)