存在同名模块时从内置库导入

Posted

技术标签:

【中文标题】存在同名模块时从内置库导入【英文标题】:Importing from builtin library when module with same name exists 【发布时间】:2011-08-27 06:11:26 【问题描述】:

情况: - 我的 project_folder 中有一个名为 calendar 的模块 - 我想使用 Python 库中的内置日历类 - 当我使用 from calendar import Calendar 时,它会抱怨,因为它试图从我的模块加载。

我进行了几次搜索,但似乎找不到解决问题的方法。

How to access a standard-library module in Python when there is a local module with the same name? http://docs.python.org/whatsnew/2.5.html How to avoid writing the name of the module all the time when importing a module in python?

无需重命名我的模块有什么想法吗?

【问题讨论】:

最好不要命名模块来隐藏内置模块。 解决方案是“选择不同的名称”。您不重命名的方法是一个坏主意。为什么不能重命名模块?重命名有什么问题? 确实如此。正是因为对这个问题没有没有好的答案,所以强烈建议不要使用影子 stdlib 模块。 我避免使用相同的模块名称,因为解决方案似乎比它的价值更麻烦。谢谢! @the_drow 这个建议没有规模,纯粹而简单。 PEP328 欣然承认这一点。 【参考方案1】:

不需要更改模块的名称。相反,您可以使用 absolute_import 来更改导入行为。例如stem/socket.py,我按如下方式导入套接字模块:

from __future__ import absolute_import
import socket

这仅适用于 Python 2.5 及更高版本;它启用了 Python 3.0 及更高版本中的默认行为。 Pylint 会抱怨代码,但它完全有效。

【讨论】:

这对我来说似乎是正确的答案。请参阅2.5 changelog 或PEP328 了解更多信息。 这是正确的解决方案。不幸的是,当启动包内的代码时,它不起作用,因为包不会被识别,并且本地路径被添加到PYTHONPATH。 Another question 展示了如何解决这个问题。 这就是解决方案。我检查了 Python 2.7.6,这是必需的,它仍然不是默认值。 确实:根据docs.python.org/2/library/__future__.html,第一个默认此行为的python版本是3.0 那么不要将你的 main 模块命名为与内置模块冲突的模块。【参考方案2】:

其实解决这个问题比较容易,但是实现总是有点脆弱,因为它依赖于python导入机制的内部,并且在未来的版本中可能会发生变化。

(以下代码显示了如何加载本地和非本地模块以及它们如何共存)

def import_non_local(name, custom_name=None):
    import imp, sys

    custom_name = custom_name or name

    f, pathname, desc = imp.find_module(name, sys.path[1:])
    module = imp.load_module(custom_name, f, pathname, desc)
    f.close()

    return module

# Import non-local module, use a custom name to differentiate it from local
# This name is only used internally for identifying the module. We decide
# the name in the local scope by assigning it to the variable calendar.
calendar = import_non_local('calendar','std_calendar')

# import local module normally, as calendar_local
import calendar as calendar_local

print calendar.Calendar
print calendar_local

如果可能的话,最好的解决方案是避免使用与标准库或内置模块名称相同的名称来命名您的模块。

【讨论】:

这将如何与sys.modules 交互以及随后尝试加载本地模块? @Omnifarious:它将模块添加到 sys.modules 及其名称,这将阻止加载本地模块。您始终可以使用自定义名称来避免这种情况。 @Boaz Yaniv:您应该为本地日历使用自定义名称,而不是标准名称。其他 Python 模块可能会尝试导入标准模块。如果你这样做了,你实现的基本上是重命名本地模块,而不必重命名文件。 @Omnifarious:无论哪种方式都可以。其他一些代码可能会尝试加载本地模块并得到相​​同的错误。您必须做出妥协,由您决定支持哪个模块。 感谢博阿斯!虽然您的 sn-p 更短(和文档),但我认为重命名模块比使用一些可能会在未来混淆人们(或我自己)的 hacky 代码更容易。【参考方案3】:

解决此问题的唯一方法是自己劫持内部导入机制。这并不容易,而且充满危险。你应该不惜一切代价避免圣杯形状的信标,因为危险太危险了。

改为重命名您的模块。

如果您想了解如何劫持内部导入机制,您可以在这里了解如何做到这一点:

The Importing Modules section of the Python 2.7 documentation The Importing Modules section of the Python 3.2 documentation PEP 302 - New Import Hooks

有时有充分的理由陷入这种危险之中。你给出的理由不在其中。重命名你的模块。

如果你走上危险的道路,你会遇到的一个问题是,当你加载一个模块时,它会以一个“官方名称”结束,这样 Python 就可以避免再次解析该模块的内容。模块的“官方名称”到模块对象本身的映射可以在sys.modules中找到。

这意味着如果你在一个地方import calendar,任何导入的模块都将被认为是正式名称为calendar的模块,以及在其他任何地方对import calendar的所有其他尝试,包括在其他代码中的主要 Python 库,将获得该日历。

也许可以使用 Python 2.x 中的 imputil module 设计一个客户导入器,这会导致从某些路径加载的模块首先在 sys.modules 以外的其他地方查找它们正在导入的模块或类似的东西。但这是一件非常麻烦的事情,而且无论如何它在 Python 3.x 中都行不通。

你可以做一件极其丑陋和可怕的事情,它不涉及挂钩导入机制。这是您可能不应该做的事情,但它可能会起作用。它将您的calendar 模块变成系统日历模块和日历模块的混合体。感谢Boaz Yaniv 提供skeleton of the function I use。将其放在您的 calendar.py 文件的开头:

import sys

def copy_in_standard_module_symbols(name, local_module):
    import imp

    for i in range(0, 100):
        random_name = 'random_name_%d' % (i,)
        if random_name not in sys.modules:
            break
        else:
            random_name = None
    if random_name is None:
        raise RuntimeError("Couldn't manufacture an unused module name.")
    f, pathname, desc = imp.find_module(name, sys.path[1:])
    module = imp.load_module(random_name, f, pathname, desc)
    f.close()
    del sys.modules[random_name]
    for key in module.__dict__:
        if not hasattr(local_module, key):
            setattr(local_module, key, getattr(module, key))

copy_in_standard_module_symbols('calendar', sys.modules[copy_in_standard_module_symbols.__module__])

【讨论】:

imputil 被视为已弃用。您应该使用imp 模块。 顺便说一句,它与 Python 3 完美兼容。而且根本不用那么毛茸茸。但是您应该始终注意,依赖于 python 以一种方式处理路径或以该顺序查找模块的代码迟早会中断。 是的,但在这种孤立的情况下(模块名称冲突),挂钩导入机制是一种矫枉过正。而且由于它多毛且不相容,所以最好不要管它。 @jspacek 不,到目前为止一切都很好,但是只有在使用 PyDev 的调试器时才会发生冲突,而不是经常使用。并确保您检查最新的代码(github 中的 URL),因为它与上述答案有所不同 @jspacek:这是一个游戏,而不是一个库,所以在我看来,向后兼容性根本不是问题。并且命名空间冲突仅在使用通过 PyDev IDE(它使用 Python 的 code std 模块)运行时发生,这意味着只有一小部分 developers 可能对这种“合并黑客”有任何问题。用户根本不会受到影响。【参考方案4】:

接受的解决方案包含现已弃用的方法。

importlib 文档here 提供了一个很好的示例,说明了直接从 python >= 3.5 的文件路径加载模块的更合适方法:

import importlib.util
import sys

# For illustrative purposes.
import tokenize
file_path = tokenize.__file__  # returns "/path/to/tokenize.py"
module_name = tokenize.__name__  # returns "tokenize"

spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)

因此,您可以从路径加载任何 .py 文件并将模块名称设置为您想要的任何名称。因此,只需将 module_name 调整为您希望模块在导入时具有的任何自定义名称。

要加载一个包而不是单个文件,file_path 应该是包的根路径__init__.py

【讨论】:

像魅力一样工作...在开发库时使用它进行测试,因此我的测试始终使用开发版本而不是发布(和安装)的版本。在 Windows 10 中,我必须像这样编写模块的路径:file_path=r"C:\Users\My User\My Path\Module File.py"。然后我像发布的模块一样调用module_name,这样我就有了完整的工作脚本,剥离了这个sn-p,可以在其他电脑上使用 默认的python模块在哪里?你怎么知道它在每个系统上都是一样的?该解决方案是否可移植? 第一句应该被删除......否则它会让人们认为这个解决方案包含一个现已弃用的方法。 但是如何从标准库中导入模块呢? 这怎么可能是一个公认的答案?问题是从标准库导入,而不是用户代码。【参考方案5】:

我想提供我的版本,它结合了 Boaz Yaniv 和 Omnifarious 的解决方案。它将导入模块的系统版本,与之前的答案有两个主要区别:

支持“点”表示法,例如。 package.module 是系统模块上 import 语句的直接替换,这意味着您只需替换那一行,如果已经对模块进行了调用,它们将按原样工作

把它放在可以访问的地方,这样你就可以调用它(我的 __init__.py 文件中有我的):

class SysModule(object):
    pass

def import_non_local(name, local_module=None, path=None, full_name=None, accessor=SysModule()):
    import imp, sys, os

    path = path or sys.path[1:]
    if isinstance(path, basestring):
        path = [path]

    if '.' in name:
        package_name = name.split('.')[0]
        f, pathname, desc = imp.find_module(package_name, path)
        if pathname not in __path__:
            __path__.insert(0, pathname)
        imp.load_module(package_name, f, pathname, desc)
        v = import_non_local('.'.join(name.split('.')[1:]), None, pathname, name, SysModule())
        setattr(accessor, package_name, v)
        if local_module:
            for key in accessor.__dict__.keys():
                setattr(local_module, key, getattr(accessor, key))
        return accessor
    try:
        f, pathname, desc = imp.find_module(name, path)
        if pathname not in __path__:
            __path__.insert(0, pathname)
        module = imp.load_module(name, f, pathname, desc)
        setattr(accessor, name, module)
        if local_module:
            for key in accessor.__dict__.keys():
                setattr(local_module, key, getattr(accessor, key))
            return module
        return accessor
    finally:
        try:
            if f:
                f.close()
        except:
            pass

示例

我想导入 mysql.connection,但我有一个本地包已经称为 mysql(官方 mysql 实用程序)。所以为了从系统mysql包中获取连接器,我替换了这个:

import mysql.connector

有了这个:

import sys
from mysql.utilities import import_non_local         # where I put the above function (mysql/utilities/__init__.py)
import_non_local('mysql.connector', sys.modules[__name__])

结果

# This unmodified line further down in the file now works just fine because mysql.connector has actually become part of the namespace
self.db_conn = mysql.connector.connect(**parameters)

【讨论】:

【参考方案6】:

更改导入路径:

import sys
save_path = sys.path[:]
sys.path.remove('')
import calendar
sys.path = save_path

【讨论】:

这行不通,因为这样做之后,如果不自己动手摆弄导入机制,就无法导入本地模块。 @Omnifarious:这是一个不同的问题,你可以通过第三个模块来解决这个问题,该模块从日历导入 *。 不,这可能行不通,因为python将模块名称缓存在sys.modules中,它不会再次导入同名模块。

以上是关于存在同名模块时从内置库导入的主要内容,如果未能解决你的问题,请参考以下文章

模块标准库内置模块

Python基础

常用工具包(模块)

在嵌入式 Python 中禁用内置模块导入

Python基础----import模块导入和包的调用

动态导入在运行时从编译输出中指定的模块