从模块反向导入*

9 投票
4 回答
675 浏览
提问于 2025-04-17 18:18

我有一个代码库,里面有些之前开发者做的决定很乱,我正在进行整理。经常会看到他做了类似这样的事情:

from scipy import *
from numpy import *

...这当然会让命名空间变得混乱,让人很难知道模块中的某个属性最初是来自哪里。

有没有办法让Python帮我分析并修复这些问题?有没有人做过这样的工具?如果没有的话,这样的工具应该怎么制作呢?

4 个回答

3

我觉得PurityLake和Martijn Pieters提供的手动辅助解决方案可能是最好的方法。不过,这个事情并不是说完全不可能用程序来实现。

首先,你需要获取一个列表,里面包含模块字典中所有可能在代码中使用的名称。我假设你的代码没有直接调用任何双下划线函数之类的东西。

接下来,你需要遍历这些名称,使用inspect.getmodule()来找出每个对象最初是在哪个模块中定义的。我假设你没有使用过那种双重导入的方式,比如from foo import *。你需要列出所有在numpyscipy模块中定义的名称。

现在,你可以把这个输出结果拿来,把每个foo替换成numpy.foo

所以,把这些放在一起,像这样:

for modname in sys.argv[1:]:
    with open(modname + '.py') as srcfile:
        src = srcfile.read()
    src = src.replace('from numpy import *', 'import numpy')
    src = src.replace('from scipy import *', 'import scipy')
    mod = __import__(modname)
    for name in dir(mod):
        original_mod = inspect.getmodule(getattr(mod, name))
        if original_mod.__name__ == 'numpy':
            src = src.replace(name, 'numpy.'+name)
        elif original_mod.__name__ == 'scipy':
            src = src.replace(name, 'scipy.'+name)
    with open(modname + '.tmp') as dstfile:
        dstfile.write(src)
    os.rename(modname + '.py', modname + '.bak')
    os.rename(modname + '.tmp', modname + '.py')

如果以上假设有任何不对的地方,修改代码也不难。此外,你可能想用tempfile.NamedTemporaryFile和其他一些改进措施,确保不会意外用临时文件覆盖原有文件。(我只是懒得处理跨平台的问题;如果你不是在Windows上运行,这个问题就简单多了。)当然,还要加上一些错误处理,可能还需要一些报告功能。

3

是的。你需要去掉那些导入的部分,然后对这个模块运行一个代码检查工具。

我推荐使用flake8,不过它可能会给你带来很多关于代码风格的警告。

光是去掉导入的部分,然后尝试运行代码,可能不够,因为很多命名错误只有在你运行特定的代码行和特定的输入时才会被发现。代码检查工具会分析代码,提前找出可能的NameError,而不需要实际运行代码。

这都是在假设没有可靠的单元测试,或者测试覆盖率不够的情况下。

在这种情况下,如果有多个from module import *的行,就会变得有点麻烦,因为你需要找出每个缺失的名称是哪个模块提供的。这需要手动去查,但你可以在Python解释器中导入这个模块,测试一下缺失的名称是否在那个模块中定义:

>>> import scipy, numpy
>>> 'loadtxt' in dir(numpy)
True

你还需要注意,在这个特定情况下,numpyscipy模块之间有重叠;对于在这两个模块中都定义的名称,最后导入的模块会“胜出”。

请记住,保留任何from module import *行会导致代码检查工具无法检测出哪些名称可能会引发NameErrors!

0

我现在做了一个小工具,叫做“dedazzler”。这个工具可以找到那些写成“from module import *”的代码行,然后把目标模块里的内容展开,替换掉这些行。

运行完这个工具后,你还需要再用一个代码检查工具(linter)来检查代码。这里有一段特别有趣的代码:

import re

star_match = re.compile('from\s(?P<module>[\.\w]+)\simport\s[*]')
now = str(time.time())
error = lambda x: sys.stderr.write(x + '\n')

def replace_imports(lines):
    """
    Iterates through lines in a Python file, looks for 'from module import *'
    statements, and attempts to fix them.
    """
    for line_num, line in enumerate(lines):
        match = star_match.search(line)
        if match:
            newline = import_generator(match.groupdict()['module'])
            if newline:
                lines[line_num] = newline
    return lines

def import_generator(modulename):
    try:
        prop_depth = modulename.split('.')[1:]
        namespace = __import__(modulename)
        for prop in prop_depth:
            namespace = getattr(namespace, prop)
    except ImportError:
        error("Couldn't import module '%s'!" % modulename)
        return
    directory = [ name for name in dir(namespace) if not name.startswith('_') ]
    return "from %s import %s\n"% (modulename, ', '.join(directory))

我把这个工具维护成了一个更实用的独立工具,可以在这里找到:

https://github.com/USGM/dedazzler/

撰写回答