Importa dinamicamente il modulo dalla memoria in Python 3 utilizzando Hooks

Nov 25 2020

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

1 ZachGates Nov 27 2020 at 08:28

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 di find_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)