Importer dynamiquement un module depuis la mémoire dans Python 3 à l'aide de Hooks

Nov 25 2020

Ce que je veux réaliser, c'est exactement ce que cette réponse propose, cependant en Python 3.

Le code ci-dessous fonctionne correctement dans 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

Cependant, lorsque vous apportez les modifications évidentes à Python 3 (en incluant exec et print entre parenthèses), j'obtiens le code suivant:

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 pas que le exec()changement ait été assez significatif. Je n'ai pas compris ce que faisait cette ligne dans Python 2, je l'ai "traduite" comme je pense qu'elle est correcte. Cependant, le code Python 3 me donne l'erreur suivante:

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'

Que dois-je changer dans le code pour travailler dans Python 3 exactement de la même manière que dans Python 2?

Observation: Cela ne répond pas à ma question car je ne suis pas intéressé par l'importation d'un module depuis .pyc.

Réponses

1 ZachGates Nov 27 2020 at 08:28

La réponse courte est que vous avez oublié de traduire la dernière moitié de l' execinstruction à partir de l'exemple de code. Cela fait execappliquer inle contexte de la load_moduleméthode - pas le new_module; alors spécifiez le contexte:

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

Cependant, en utilisant une version Python 3.4 ou supérieure, vous devenez soumis à PEP 451 (l'introduction des spécifications du module ), ainsi qu'à la dépréciation du impmodule, en faveur de importlib. Particulièrement:

  • La imp.new_module(name)fonction est remplacée par importlib.util.module_from_spec(spec).
  • Une classe de base abstraite pour les objets finder chemin méta est fourni: importlib.abc.MetaPathFinder.
  • Et ces objets de recherche utilisent désormais find_specplutôt que find_module.

Voici une réimplémentation très proche de l'exemple de code.

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)