PyQt4.QtCore的导入钩子

10

我试图通过sys.meta_path设置一些导入钩子,类似于这个SO问题的方法。为此,我需要定义两个函数find_moduleload_module,如上面的链接所解释的那样。这是我的load_module函数:

import imp

def load_module(name, path):
    fp, pathname, description = imp.find_module(name, path)

    try:
        module = imp.load_module(name, fp, pathname, description)
    finally:
        if fp:
             fp.close()
    return module

这对大多数模块都有效,但在使用Python 2.7时,对于PyQt4.QtCore失败了:

name = "QtCore"
path = ['/usr/lib64/python2.7/site-packages/PyQt4']

mod = load_module(name, path)

返回

Traceback (most recent call last):
   File "test.py", line 19, in <module>
   mod = load_module(name, path)
   File "test.py", line 13, in load_module
   module = imp.load_module(name, fp, pathname, description)
SystemError: dynamic module not initialized properly

同样的代码在Python 3.4中运行良好(尽管imp被弃用,应该使用importlib代替)。我想这可能与SIP动态模块初始化有关。在Python 2.7中是否还有其他我应该尝试的东西?
注意:这适用于PyQt4PyQt5编辑:这可能与此问题有关,因为实际上,
cd /usr/lib64/python2.7/site-packages/PyQt4
python2 -c 'import QtCore'

出现了相同的错误。 我仍然不确定有什么方法可以解决它...

编辑2: 以下是@Nikita要求的具体用例示例,我试图做的是重定向导入,因此当执行import A时,发生的情况是import B。 人们可能确实认为,在find_spec/find_module中进行模块重命名,然后使用默认的load_module就足够了。 然而,在Python 2中,不清楚在哪里找到默认的load_module实现。 我找到的与之类似的最接近的实现是future.standard_library.RenameImport。 看起来没有将Python 3中的完整importlib实现移植到2中的后备。

可以在此gist中找到重现此问题的导入钩子的最小工作示例。


如果需要一些通用的背景信息来解释我正在尝试做什么,可以参考SiQt软件包,并且这个问题在这个Github问题中有所讨论。 - rth
我真的不理解你的问题,但是 __import__('PyQt4.QtCore') 有什么问题吗?会导致无限递归吗? - danidee
@danidee __import__('A') 没有问题,但它等同于使用 import A。我想要的是在你执行 import A 时改变发生的事情,特别是运行 import B。这可以通过 sys.meta_path 中的导入钩子来完成,但它们需要更低级别的函数,如 imp.load_module - rth
@rth,确实在Python 2.7的imporylib文档中写道:“这个模块是Python 3.1中同名更全面的包中可用内容的一个小子集,它提供了import的完整实现。”关于自定义导入的想法在PEP302中有所涉及,我会研究一下并在答案更新中分享我的想法。 - Nikita
2个回答

4

更新:此部分在答案更新后实际上并不相关,请查看下面的更新。

为什么不直接使用Python 2.7和Python 3中都可用的importlib.import_module

#test.py

import importlib

mod = importlib.import_module('PyQt4.QtCore')
print(mod.__file__)

在Ubuntu 14.04上:

$ python2 test.py 
/usr/lib/python2.7/dist-packages/PyQt4/QtCore.so

由于它是一个动态模块,就像错误中所说的一样(实际文件是QtCore.so),也可以看一下 imp.load_dynamic
另一个解决方案可能是强制执行模块初始化代码,但我认为这太麻烦了,为什么不使用 importlib 呢。 更新:在pkgutil中有些东西可能会有所帮助。我在评论中谈到的是,试着修改你的查找器,就像这样:
import pkgutil

class RenameImportFinder(object):

    def find_module(self, fullname, path=None):
        """ This is the finder function that renames all imports like
             PyQt4.module or PySide.module into PyQt4.module """
        for backend_name in valid_backends:
            if fullname.startswith(backend_name):
                # just rename the import (That's what i thought about)
                name_new = fullname.replace(backend_name, redirect_to_backend)
                print('Renaming import:', fullname, '->', name_new, )
                print('   Path:', path)


                # (And here, don't create a custom loader, get one from the
                # system, either by using 'pkgutil.get_loader' as suggested
                # in PEP302, or instantiate 'pkgutil.ImpLoader').

                return pkgutil.get_loader(name_new) 

                #(Original return statement, probably 'pkgutil.ImpLoader'
                #instantiation should be inside 'RenameImportLoader' after
                #'find_module()' call.)
                #return RenameImportLoader(name_orig=fullname, path=path,
                #       name_new=name_new)

    return None

目前无法测试上述代码,请自行尝试。

P.S. 注意,Python 3中能用的imp.load_module()在Python 3.3之后已被弃用

另一种解决方案是不使用钩子,而是包装__import__

print(__import__)

valid_backends = ['shelve']
redirect_to_backend = 'pickle'

# Using closure with parameters 
def import_wrapper(valid_backends, redirect_to_backend):
    def wrapper(import_orig):
        def import_mod(*args, **kwargs):
            fullname = args[0]
            for backend_name in valid_backends:
                if fullname.startswith(backend_name):
                    fullname = fullname.replace(backend_name, redirect_to_backend)
                    args = (fullname,) + args[1:]
            return import_orig(*args, **kwargs)
        return import_mod
    return wrapper

# Here it's important to assign to __import__ in __builtin__ and not
# local __import__, or it won't affect the import statement.
import __builtin__
__builtin__.__import__ = import_wrapper(valid_backends, 
                                        redirect_to_backend)(__builtin__.__import__)

print(__import__)

import shutil
import shelve
import re
import glob

print shutil.__file__
print shelve.__file__
print re.__file__
print glob.__file__

输出:

<built-in function __import__>
<function import_mod at 0x02BBCAF0>
C:\Python27\lib\shutil.pyc
C:\Python27\lib\pickle.pyc
C:\Python27\lib\re.pyc
C:\Python27\lib\glob.pyc

shelve被重命名为pickle,并且pickle通过默认机制被导入,变量名为shelve


我同意你的前两个想法,但很遗憾它们不起作用,我之前尝试过。a)据我所知,“importlib.import_module”太高级了,不能放在“sys.meta_path”导入钩子中。当你导入一个包时,它会查找“sys.meta_path”,如果“load_module”函数使用“importlib.import_module”,那么它会再次查找“sys.meta_path”,在那里它将发现相同的“load_module”函数等,因此会出现无限递归问题...所需的是一些更低层次的东西,如“imp.find_module”或“importlib.machinery.SourceFileLoader”。 - rth
b) 我已经尝试过imp.load_dynamic,它产生了相同的结果(因为我认为它必须由imp.load_module调用)。 c) 是的,我知道我最好不要手动初始化那个模块。我不明白的是为什么我必须这样做(即为什么需要importlib.import_module而不是imp.load_module)。对于所有的PyQt4/PyQt4子模块也是如此。我试图实现的目标是在导入PyQt4.QtCore时同时导入SiQt.QtCore。我知道这是可能的,因为Python 2中的future.standard_library.RenameImport就是这样做的(基本上只是导入重命名)。 - rth
1
根据您提供的关于导入钩子的链接,@rth,它说元路径查找器将为路径的每个部分递归调用find_spec/find_module。例如:mpf.find_spec("PyQt4", None, None),然后再调用一次mpf.find_spec("PyQt4.QtCore", PyQt4.__path__, None)。因此,如果您要在find_spec或mpf的其他部分中进行钩取,可以将名称字符串中的PyQt4替换为SiQt,然后调用默认机制让它自己加载SiQt。如果我理解有误,请提供用于钩子的代码以更好地理解您想要实现的内容。 - Nikita
жҲ‘еҗҢж„ҸдҪҝз”Ёload_moduleзҡ„й»ҳи®ӨжңәеҲ¶дјҡеҫҲеҘҪгҖӮиҜ·еҸӮи§ҒдёҠйқўй—®йўҳдёӯзҡ„Edit2гҖӮ - rth
@rth,如果钩子不起作用,您可以包装__import__,请查看答案更新。 - Nikita

3
当查找一个像PyQt4.QtCore这样的包中的模块时,您必须递归地查找名称的每个部分,不包括.。而imp.load_module要求其name参数是完整的模块名称,用.来分隔包和模块名称。
因为QtCore是一个包的一部分,所以应该使用python -c 'import PyQt4.QtCore'。以下是加载模块的代码。
import imp

def load_module(name):
    def _load_module(name, pkg=None, path=None):
        rest = None
        if '.' in name:
            name, rest = name.split('.', 1)
        find = imp.find_module(name, path)
        if pkg is not None:
            name = '{}.{}'.format(pkg, name)
        try:
            mod = imp.load_module(name, *find)
        finally:
            if find[0]:
                find[0].close()
        if rest is None:
            return mod 
        return _load_module(rest, name, mod.__path__)
    return _load_module(name)

测试;

print(load_module('PyQt4.QtCore').qVersion())  
4.8.6

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接