Dynamicznie importuj moduł z pamięci w Pythonie 3 za pomocą hooków
To, co chcę osiągnąć, jest dokładnie tym, co proponuje ta odpowiedź , jednak w Pythonie 3.
Poniższy kod działa dobrze w Pythonie 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
Jednak podczas dokonywania oczywistych zmian w Pythonie 3 (zamykając exec i print w nawiasach) otrzymuję następujący kod:
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
Nie żeby exec()
zmiana była dość znacząca. Nie rozumiałem, co robi ta linia w Pythonie 2, "przetłumaczyłem" ją tak, jak myślę, że jest poprawna. Jednak kod Python 3 daje mi następujący błąd:
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'
Co powinienem zmienić w kodzie, aby działał w Pythonie 3 dokładnie tak samo, jak działa w Pythonie 2?
Obserwacja: To nie odpowiada na moje pytanie, ponieważ nie jestem zainteresowany importowaniem modułu z .pyc
.
Odpowiedzi
Krótka odpowiedź jest taka, że zapomniałeś przetłumaczyć drugą połowę exec
instrukcji z próbki kodu. To powoduje, że exec
zastosowanie in
ma kontekst load_module
metody, a nie new_module
; więc określ kontekst:
exec(self._modules[fullname], new_module.__dict__)
Jednak korzystając z Pythona w wersji 3.4 lub nowszej, stajesz się przedmiotem PEP 451 (wprowadzenie specyfikacji modułów ), a także wycofania impmodułu na korzyść importlib. Szczególnie:
- imp.new_module(name)Funkcja zastępuje
importlib.util.module_from_spec(spec)
. - Abstrakcyjną klasą bazową dla meta toru obiektów Finder jest dostarczany:
importlib.abc.MetaPathFinder
. - A takie obiekty wyszukiwarki używają teraz
find_spec
zamiastfind_module
.
Oto bardzo dokładna reimplementacja przykładu kodu.
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)