从模块导入反转 *

Posted

技术标签:

【中文标题】从模块导入反转 *【英文标题】:Reversing from module import * 【发布时间】:2013-02-23 03:01:51 【问题描述】:

我有一个代码库,我正在清理以前开发人员的一些混乱决定。他经常这样做:

from scipy import *
from numpy import *

...这当然会污染名称空间,并且很难分辨模块中的属性最初来自哪里。

有什么方法可以让 Python 为我分析和解决这个问题?有没有人为此做了一个实用程序?如果没有,如何制作这样的实用程序?

【问题讨论】:

我对你有感觉。希望你能找到一些不错的工具。 (+1) 更好的是,我希望你写一个很好的工具(无论是否基于我的回答)并在 PyPI 上发布,所以如果我需要这样的东西,我不必自己做. :) 也看到这个问题:Is there an IDE/utility to refactor Python * imports to use standard module.member syntax? 【参考方案1】:

我认为 PurityLake 和 Martijn Pieters 的辅助手动解决方案可能是最好的方法。但以编程方式执行此操作并非不可能

首先,您需要获取模块字典中可能在代码中使用的所有名称的列表。我假设您的代码没有直接调用任何 dunder 函数等。

然后,您需要遍历它们,使用inspect.getmodule() 找出每个对象最初定义在哪个模块中。我假设您没有使用任何被加倍from foo import *-ed 的东西。列出在 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 上运行,这很容易。)显然,添加一些错误处理,可能还有一些报告。

【讨论】:

我认为这不会奏效。名为my_foo 的变量呢?突然间你得到my_numpy.foo。哎呀。当然,如果有一个合适的解析器(我在想ast),你可能会这样做。 这可能是我想要的更多。这家伙偶尔有模块,其中进行了五种不同的通配符导入。这是精神错乱。这里的搜索和替换有点危险,因为它做了一些假设,但这个想法让我朝着正确的方向前进。 最终,这是一个对整个社区有用的工具的合理开始。我也想知道这种事情会如何与__all__ 交互。要真正做到正确,您可能需要过滤掉 module.__all__ 中不存在的内容(如果存在)。 @mgilson:你是对的,如果你有 both my_foofoo,就没有办法在不破坏另一个的情况下修复一个。你甚至不需要一个合适的解析器,只需要一个词法分析器,这要简单得多。但你是对的,我们已经在ast拥有一个合适的解析器,所以也许这就是要走的路。 @mgilson:我最初的想法是我不会这样做——我会依赖 linter 和单元测试以及手动编辑,就像 Martijn Pieters 的回答一样。但是既然你提到了它,如果有人接受它并对其进行改进,它可能是一个有用的通用工具。【参考方案2】:

是的。删除导入并在模块上运行 linter。

我建议使用flake8,尽管它也可能会产生很多关于样式错误的噪音。

仅仅删除导入并尝试运行代码可能是不够的,因为在您使用正确的输入运行正确的代码行之前,不会引发许多名称错误。相反,linter 将通过解析来分析代码,并且无需运行代码即可检测潜在的NameErrors。

这一切都假定没有可靠的单元测试,或者测试没有提供足够的覆盖率。

在这种情况下,如果有 多个 from module import * 行,它会变得更加痛苦,因为您需要为每个缺失的名称找出哪个模块提供了该名称。这将需要手动工作,但您可以简单地在 python 解释器中导入模块并测试是否在该模块上定义了缺少的名称:

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

您确实需要考虑到在这种特定情况下,numpyscipy 模块之间存在重叠;对于两个模块中定义的任何名称,最后导入的模块获胜。

请注意,保留 any from module import * 行意味着 linter 将无法检测到哪些名称可能引发 NameErrors!

【讨论】:

如果我删除导入,这将如何告诉我名称来自哪个模块?它不会只是对现在未列出的所有属性的名称错误吗? @Kelketek:是的,你必须弄清楚每个模块应该来自哪个模块。幸运的是,这并不难。 如果一次删除一个,这可能会更容易。 大概,我应该先运行 linter 以获取它可能发现的任何一般问题,解决它们,删除一个导入行,再次运行它以查看哪些名称作为名称错误弹出,然后添加它们到“从”导入,然后对每个通配符导入重复该过程? @MartijnPieters:这实际上可能会非常痛苦。以 OP 的 from scipy import *from numpy import * 为例,一些函数的语义,例如 log10 将取决于最后一个导入(它们存在于两个模块中,但行为不同)。见***.com/questions/6200910/…【参考方案3】:

我现在制作了一个小实用程序来执行此操作,我称之为“dedazzler”。它会找到'from module import *'的行,然后展开目标模块的'dir',替换这些行。

运行它之后,你仍然需要运行一个 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/

【讨论】:

【参考方案4】:

好的,这就是我认为你可以做的,打破程序。删除导入并注意所犯的错误。然后只导入你想要的模块,这可能需要一段时间,但这是我知道的唯一方法,如果有人知道有帮助的工具,我会很高兴

编辑: 啊,是的,一个短绒,我没想到。

【讨论】:

以上是关于从模块导入反转 *的主要内容,如果未能解决你的问题,请参考以下文章

如何从延迟加载的模块访问 AppModule 模块导入?

python导入模块的方法都有哪些

在 Python 中从导入的模块导入时输出 [重复]

从模块中动态导入所有内容 ( * )

如何找出从哪个模块导入名称?

Python模块导入详解