当我的项目有一个同名的模块时,我该如何从标准库导入?(我如何控制 Python 在哪里查找模块?)

2025-01-03 08:41:00
admin
原创
95
摘要:问题描述:我的项目文件夹中有一个名为 的模块calendar。在代码的其他地方,我想使用标准库Calendar类。但是当我尝试使用 导入此类时,from calendar import Calendar它会从我自己的模块导入,从而导致稍后出现错误。我该如何避免这种情况?我必须重命名模块吗?解决方案 1:无需重...

问题描述:

我的项目文件夹中有一个名为 的模块calendar。在代码的其他地方,我想使用标准库Calendar类。但是当我尝试使用 导入此类时,from calendar import Calendar它会从我自己的模块导入,从而导致稍后出现错误。

我该如何避免这种情况?我必须重命名模块吗?


解决方案 1:

无需重命名模块。相反,在 Python 2.5 及更高版本中,使用absolute_import来更改导入行为。

例如要导入标准库socket模块,即使socket.py项目中有一个:

from __future__ import absolute_import
import socket

在 Python 3.x 中,此行为是默认行为。Pylint 会对代码提出抱怨,但这是完全有效的。

解决方案 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

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

解决方案 3:

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

重命名您的模块。

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

  • Python 2.7 文档的导入模块部分

  • Python 3.2 文档的导入模块部分

  • PEP 302 - 新的导入钩子

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

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

这意味着如果您import calendar在一个地方,任何导入的模块都将被视为具有官方名称的模块calendar,并且所有其他import calendar任何地方的尝试,包括作为主 Python 库一部分的其他代码,都将获得该日历。

也许可以使用Python 2.x 中的imputil 模块设计一个客户导入器,使从某些路径加载的模块在除 first 之外的其他位置或类似位置查找它们要导入的模块sys.modules。但这是极其棘手的事情,而且无论如何它在 Python 3.x 中都行不通。

有一件非常丑陋和可怕的事情你可以做,它不涉及挂钩导入机制。你可能不应该这样做,但它可能会起作用。它会将您的calendar模块变成系统日历模块和日历模块的混合体。感谢Boaz Yaniv提供我使用的函数的骨架。将其放在文件的开头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__])

解决方案 4:

在 Python 3.5 及更高版本中,使用标准库importlib模块直接从指定路径导入,绕过import的查找机制:

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)

在实际代码中,file_path可以设置为.py要导入的文件的任何路径;module_name应该是要导入的模块的名称(导入系统在import尝试进一步的语句时使用该名称来查找模块)。后续代码将用作模块的名称;将变量名称module更改为使用不同的名称。 module

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

解决方案 5:

序言:一些术语

绝对导入是指 Python 在所谓的系统模块路径(SMP) 中逐个搜索文件夹,直到找到包含该模块的文件夹。

SMP 是在启动时根据环境变量和其他一些内容创建的PYTHONPATH。它在 Python 中表示为字符串列表。导入sys标准库模块后,它可用作sys.path,但无论该模块是否被导入,它都存在。(通常,Python 程序员只将此列表称为“sys.path”。但是,由于我们正在深入讨论导入机制的技术细节,并使用示例中命名的模块sys,因此建立一个单独的术语似乎很合适。)

另一方面,相对导入直接指定模块的代码相对于当前模块在当前包上下文中的位置。


概括

我们如何从标准库而不是当前包中导入?

如果所讨论的标准库模块未实现为内置的,则可能需要在启动 Python 之前设置PYTHONSAFEPATH环境变量。这可防止 Python 将主脚本的目录(当像 一样启动时python script.py)或当前工作目录(否则)放在 SMP 的启动位置,而默认情况下会这样做。

在 3.11 中,可以使用 -P 选项来代替设置PYTHONSAFEPATH。在 3.4 及更高版本中,也可以使用 -I,但这也会忽略其他特定于 Python 的环境变量,并跳过将每个用户的站点包目录添加到 SMP。(当然,也可以通过sys.path编程方式进行修改以修复 SMP。当然,除非问题出在尝试导入sys。)

一旦解决了这个问题,只需使用绝对导入。

在 3.x 中:

import sys # or
from sys import version # or
from sys import * # but beware namespace pollution

在 2.5 到 2.7 中,__future__首先需要导入:

from __future__ import absolute_import
# proceed as above

在 2.4 及以下版本中,需要进行一些 hack 来防止隐式相对导入:

sys = __import__("sys", {})

这也应该可以在其他版本中工作,但是比必要的更丑陋和更复杂。

或者,尝试挂接到导入机制以更改查找行为,如Omnifarious 的答案、Boaz Yaniv 的答案或casey 的答案中所述。所有这些方法都通过模拟(部分)用于搜索模块的内部算法来工作,但跳过相对导入步骤并跳过 的第一个元素sys.path。(前提是sys.path认为是“不安全的” - 根据上面的讨论PYTHONSAFEPATH- 因此使用该路径将在项目内查找。)

我们如何防止标准库从当前包导入,并确保它从自身导入?

标准库通常使用绝对导入。如果由于当前包的遮蔽而导致问题,标准建议是重命名当前包中的模块。如果做不到这一点,相同的环境变量和命令行标志技巧应该有效。

我们如何从当前包而不是标准库中导入?

在 2.4 和更早版本中,这将默认发生。

从 2.5 开始,在确保包设置正确后,使用相对导入。

在大多数情况下,这意味着确保软件包的根目录位于 SMP 上。最可靠的方法是激活虚拟环境并在该虚拟环境中安装软件包-m。否则,假设程序从某个“驱动程序”脚本启动,请确保它与软件包根目录位于同一文件夹中。或者,使用标志从包含软件包根目录的目录中将软件包(或具有适当点路径名的子软件包)作为模块运行。

假设包设置正确,相对导入如下所示:

from . import sys # sys.py in the same folder as this source file
from .sys import something # from our source, not the standard library
from .sys import * # again, beware namespace pollution
from .child import sys # child/sys.py
from .. import sys # ../sys.py, IF .. is still within the package root
from ..sibling import sys # ../sibling/sys.py
# Use more .'s to go up more levels first.
# This will not work beyond the package root.

请注意,所有这些都使用from语法。如PEP 中所述:

相对导入必须始终使用from <> importimport <>始终是绝对的....因为在import XXX.YYY.ZZZ...之后XXX.YYY.ZZZ可用于表达式。但.moduleY不能用于表达式。

作为绝对的最后手段sys.path,在确定当前文件的路径并计算出包根路径后,可以通过 有意修改 SMP ,以便绝对导入可以正常工作。请注意,在一般情况下,这根本不是必要的。许多流行的、重型的 Python 库跨越了数十万行代码,但绝对没有一行sys.path以任何方式提及。

我们如何才能诱使标准库从当前包中导入,而不是像它设计的那样从自身导入?

我暂时还不知道 Python 标准库在内部使用相对导入的任何地方,我也想不出尝试这样做的充分理由。话虽如此,通过破解导入机制可能可以实现。这听起来既不容易也不好玩,我不会在这里尝试。

我们如何从其他地方进口?

请参阅如何在给定完整路径的情况下动态导入模块?。 Brandon Squizzato 的回答也总结了常用技巧。


Python 在哪里寻找模块

2.4 及之前版本

最初,相对导入没有特殊语法。Python 会先尝试相对导入,如果失败则尝试绝对导入。例如,类似这样的代码会从与当前模块相同的文件夹import sys导入(如果存在),而不是从标准库模块导入。sys.py

我的理解是,这种相对导入是相对于包根,而不是当前模块;但目前我无法轻易验证这一点。

显然可以解决这个问题,通过为上下文提供一个空的字典globals__import__引用:

sys = __import__("sys", {})

import语句使用全局命名空间来确定在哪个包上调用它;如果传递一个空的命名空间,它就无法推断包信息。

因此,通过__import__直接使用该函数,可以跳过尝试的相对导入。

2.5至2.7

PEP 328提出了一项提案,让 Python 导入默认为绝对导入,并仅在具有明确语法的情况下使用相对导入。(它还向 Python 生态系统引入了术语“绝对导入”和“相对导入”。)使用此语法的相对导入是相对于执行导入的模块的;.可以使用额外的前导 s 来指定父包。

从 Python 2.5 的第一个版本开始,通过使用__future__import就可以实现新的行为:

from __future__ import absolute_import
import math # checks the SMP

另一方面,

from __future__ import absolute_import
from . import math # imports from the same directory as this source file

或者

from __future__ import absolute_import
from .. import math # imports from the immediate parent directory,
                    # IF it is still within the package root

3.x 及以上版本

PEP 328 中描述的行为成为默认行为。2.4 的隐式相对导入不再可用,并且代码明确指定绝对或相对导入 - 无需__future__导入。


仅选择绝对或相对导入是不够的

此时出现的一些问题

  1. 显然,由于软件包系统的微妙性,许多人发现相对导入很难使用。关键在于,.相对导入语法中使用的 s不是指目录树中的级别,而是指软件包树中的级别。(毕竟,有加载模块的方法根本不涉及.py磁盘上的文件!)

  2. 即使使用绝对导入,当前项目的代码仍可能意外地遮蔽标准库。这是因为 SMP 的配置方式。在大多数情况下,Python 进程的当前工作目录或从命令行启动的“驱动程序”脚本的目录将位于 SMP 上的第一个目录。因此,即使执行绝对导入,import this也可能找不到标准库this。(另一方面,import sys 找到标准库;请参阅最后一节。)

  3. 同时,标准库并没有很好地利用打包的优势。它没有自己的根包;而且大多数情况下,当标准库模块相互依赖时,它们都使用绝对导入。

最后两点可以以令人惊讶的方式相互作用:

$ touch token.py
$ python
Python 3.8.10 (default, Nov 14 2022, 12:59:47) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> help
Type help() for interactive help, or help(object) for help about object.
>>> help()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.8/_sitebuiltins.py", line 102, in __call__
    import pydoc
  File "/usr/lib/python3.8/pydoc.py", line 66, in <module>
    import inspect
  File "/usr/lib/python3.8/inspect.py", line 40, in <module>
    import linecache
  File "/usr/lib/python3.8/linecache.py", line 11, in <module>
    import tokenize
  File "/usr/lib/python3.8/tokenize.py", line 35, in <module>
    from token import EXACT_TOKEN_TYPES
ImportError: cannot import name 'EXACT_TOKEN_TYPES' from 'token' (/current/working/directory/token.py)

通过使用摘要中的任何一种技术修复 SMP 即可避免此问题:

$ touch token.py
$ python -I
Python 3.8.10 (default, Nov 14 2022, 12:59:47) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> help
Type help() for interactive help, or help(object) for help about object.
>>> help()

Welcome to Python 3.8's help utility!

If this is your first time using Python, you should definitely check out
the tutorial on the Internet at https://docs.python.org/3.8/tutorial/.

Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules.  To quit this help utility and
return to the interpreter, just type "quit".

To get a list of available modules, keywords, symbols, or topics, type
"modules", "keywords", "symbols", or "topics".  Each module also comes
with a one-line summary of what it does; to list the modules whose name
or summary contain a given string such as "spam", type "modules spam".

help>

这还不是全部

模块加载系统实际上比上面描述的要复杂得多。它故意充满了钩子,以便修改行为。特别是:除了 SMP,还有一个“元路径”(以 提供sys.meta_path),其中包含实际用于加载模块的模块加载器。

文档给出了近似该过程的配方。(当然,实际操作不必解决导入的引导问题sysimportlib.util也可以实现导入系统!)但是,它并没有真正展示每个人 finder in sys.meta_path在做什么。

大致来说,默认情况下,对于绝对导入:

  • 首先,_frozen_importlib.BuiltinImporter检查模块名称是否与 Python 内置模块匹配,以便可以直接导入。部分标准库以这种方式实现,部分则不是;并且该部分已随时间而变化。

  • 然后,_frozen_importlib.FrozenImporter尝试加载具有指定名称的冻结模块。

  • 最后,如果这两种方法都失败了,_frozen_importlib_external.PathFinder则搜索 SMP。

因此,用户代码可能会隐藏某些标准库模块的绝对导入,而不会隐藏其他模块。假设我们有这个测试脚本import_test.py

def test_import(name):
    module = __import__(name)
    return any(attr for attr in dir(module) if not attr.startswith('__'))

if __name__ == '__main__':
    print('imported this from standard library?', test_import('this'))
    print('imported sys from standard library?', test_import('sys'))

让我们看看如果首先将具有这些名称的空 Python 文件添加到 CWD 会发生什么:

$ touch this.py
$ touch sys.py
$ python import_test.py 
imported this from standard library? False
imported sys from standard library? True

自2.0起,sys.builtin_module_names列出 Python 内置模块的名称。此外,自 3.10 起,sys.stdlib_module_names列出标准库中所有可能的模块名称(即使是 Python 编译时故意排除的模块名称,或当前操作系统不可用的模块名称)。

解决方案 6:

我想提供我的版本,它是 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)
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用