如何在运行时检查python模块是否有效而不导入?

Mr_and_Mrs_D

我有一个包含子软件包的软件包,在运行时仅需要导入其中一个子软件包-但我需要测试它们是否有效。这是我的文件夹结构:

game/
 __init__.py
 game1/
   __init__.py
   constants.py
   ...
 game2/
   __init__.py
   constants.py
   ...

现在,在启动时运行的代码可以:

import pkgutil
import game as _game
# Detect the known games
for importer,modname,ispkg in pkgutil.iter_modules(_game.__path__):
    if not ispkg: continue # game support modules are packages
    # Equivalent of "from game import <modname>"
    try:
        module = __import__('game',globals(),locals(),[modname],-1)
    except ImportError:
        deprint(u'Error in game support module:', modname, traceback=True)
        continue
    submod = getattr(module,modname)
    if not hasattr(submod,'fsName') or not hasattr(submod,'exe'): continue
    _allGames[submod.fsName.lower()] = submod

但这有一个缺点,即所有子包都被导入,从而导入了子包中的其他模块(例如constants.py等),这相当于几兆字节的垃圾。因此,我想用一个子模块有效的测试来代替此代码(它们很好地导入)。我想我应该以某种方式使用eval-但是如何?或者我该怎么办?

编辑: tldr;

我正在寻找等效于上述循环的核心:

    try:
        probaly_eval(game, modname) # fails iff `from game import modname` fails
        # but does _not_ import the module
    except: # I'd rather have a more specific error here but methinks not possible
        deprint(u'Error in game support module:', modname, traceback=True)
        continue

所以我想要一个明确的答案,如果存在与导入语句完全相同的可视化错误检查-导入模块。那是我的问题,很多回答者和评论者回答了不同的问题。

Mr_and_Mrs_D

我们已经有一个自定义的导入器(免责声明:我没有写代码,我只是当前的维护者),其load_module

def load_module(self,fullname):
    if fullname in sys.modules:
        return sys.modules[fullname]
    else: # set to avoid reimporting recursively
        sys.modules[fullname] = imp.new_module(fullname)
    if isinstance(fullname,unicode):
        filename = fullname.replace(u'.',u'\\')
        ext = u'.py'
        initfile = u'__init__'
    else:
        filename = fullname.replace('.','\\')
        ext = '.py'
        initfile = '__init__'
    try:
        if os.path.exists(filename+ext):
            with open(filename+ext,'U') as fp:
                mod = imp.load_source(fullname,filename+ext,fp)
                sys.modules[fullname] = mod
                mod.__loader__ = self
        else:
            mod = sys.modules[fullname]
            mod.__loader__ = self
            mod.__file__ = os.path.join(os.getcwd(),filename)
            mod.__path__ = [filename]
            #init file
            initfile = os.path.join(filename,initfile+ext)
            if os.path.exists(initfile):
                with open(initfile,'U') as fp:
                    code = fp.read()
                exec compile(code, initfile, 'exec') in mod.__dict__
        return mod
    except Exception as e: # wrap in ImportError a la python2 - will keep
        # the original traceback even if import errors nest
        print 'fail', filename+ext
        raise ImportError, u'caused by ' + repr(e), sys.exc_info()[2]

所以我想我可以sys.modules用可覆盖的方法替换访问缓存的部分,这些方法在我的覆盖中将单独保留该缓存:

所以:

@@ -48,2 +55,2 @@ class UnicodeImporter(object):
-        if fullname in sys.modules:
-            return sys.modules[fullname]
+        if self._check_imported(fullname):
+            return self._get_imported(fullname)
@@ -51 +58 @@ class UnicodeImporter(object):
-            sys.modules[fullname] = imp.new_module(fullname)
+            self._add_to_imported(fullname, imp.new_module(fullname))
@@ -64 +71 @@ class UnicodeImporter(object):
-                    sys.modules[fullname] = mod
+                    self._add_to_imported(fullname, mod)
@@ -67 +74 @@ class UnicodeImporter(object):
-                mod = sys.modules[fullname]
+                mod = self._get_imported(fullname)

并定义:

class FakeUnicodeImporter(UnicodeImporter):

    _modules_to_discard = {}

    def _check_imported(self, fullname):
        return fullname in sys.modules or fullname in self._modules_to_discard

    def _get_imported(self, fullname):
        try:
            return sys.modules[fullname]
        except KeyError:
            return self._modules_to_discard[fullname]

    def _add_to_imported(self, fullname, mod):
        self._modules_to_discard[fullname] = mod

    @classmethod
    def cleanup(cls):
        cls._modules_to_discard.clear()

然后我在sys.meta_path中添加了导入器,很高兴:

importer = sys.meta_path[0]
try:
    if not hasattr(sys,'frozen'):
        sys.meta_path = [fake_importer()]
    perform_the_imports() # see question
finally:
    fake_importer.cleanup()
    sys.meta_path = [importer]

对 ?错误!

Traceback (most recent call last):
  File "bash\bush.py", line 74, in __supportedGames
    module = __import__('game',globals(),locals(),[modname],-1)
  File "Wrye Bash Launcher.pyw", line 83, in load_module
    exec compile(code, initfile, 'exec') in mod.__dict__
  File "bash\game\game1\__init__.py", line 29, in <module>
    from .constants import *
ImportError: caused by SystemError("Parent module 'bash.game.game1' not loaded, cannot perform relative import",)

Hu?我当前正在导入非常相同的模块。好吧,答案可能在导入文档中

如果在高速缓存中未找到模块,则搜索sys.meta_path(可以在PEP 302中找到sys.meta_path的规范)。

这还不完全是重点,但我是该语句from .constants import * 查找sys.modules来检查父模块是否存在,并且我看不到任何绕过该方法的方式(请注意,我们的自定义加载器正在使用内置的导入机制模块,mod.__loader__ = self是在事实之后设置的)。

因此,我更新了FakeImporter以使用sys.modules缓存,然后进行清理。

class FakeUnicodeImporter(UnicodeImporter):

    _modules_to_discard = set()

    def _check_imported(self, fullname):
        return fullname in sys.modules or fullname in self._modules_to_discard

    def _add_to_imported(self, fullname, mod):
        super(FakeUnicodeImporter, self)._add_to_imported(fullname, mod)
        self._modules_to_discard.add(fullname)

    @classmethod
    def cleanup(cls):
        for m in cls._modules_to_discard: del sys.modules[m]

但是,这以一种新的方式-或两种方式产生了爆炸:

  • 对游戏/程序bash包的引用保存在sys.modules的顶级程序包实例中:

    bash\
      __init__.py
      the_code_in_question_is_here.py
      game\
        ...
    

    因为game被导入为bash.game该引用包含对所有game1, game2,...子包的引用,因此这些子包绝不会被垃圾回收

  • 对另一个模块(brec)的引用bash.brec与同一bash模块实例一样。该引用是from .. import brec在game \ game1中导入的而不会触发import来更新SomeClass但是,在另一个模块中,表单的导入from ...brec import SomeClass 确实触发了导入,并且brec模块的另一个实例最终出现在sys.modules中。该实例未更新,SomeClass并因AttributeError而自爆。

两者都是通过手动删除这些引用来解决的-因此,gc收集了所有模块(75个内存中有5 MB内存),并且from .. import brec确实触发了导入(这from ... import foofrom ...foo import bar问题有关)。

这个故事的寓意是有可能但:

  • 包和子包应仅相互引用
  • 应该从顶级包属性中删除对外部模块/包的所有引用
  • 包引用本身应从顶级包属性中删除

如果这听起来很复杂且容易出错,那么-至少现在我对相互依赖性及其危险性有更清晰的认识-是时候解决这个问题了。


这篇文章是由Pydev的调试器赞助的-我发现该gc模块对于从这里了解正在发生的事情非常有用当然,有很多变量是调试器的变量,也很复杂

在此处输入图片说明

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章