当我的项目有一个同名的模块时,我该如何从标准库导入?(我如何控制 Python 在哪里查找模块?)
- 2025-01-03 08:41:00
- admin 原创
- 95
问题描述:
我的项目文件夹中有一个名为 的模块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 <> import
;import <>
始终是绝对的....因为在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__
导入。
仅选择绝对或相对导入是不够的
此时出现的一些问题
显然,由于软件包系统的微妙性,许多人发现相对导入很难使用。关键在于,
.
相对导入语法中使用的 s不是指目录树中的级别,而是指软件包树中的级别。(毕竟,有加载模块的方法根本不涉及.py
磁盘上的文件!)即使使用绝对导入,当前项目的代码仍可能意外地遮蔽标准库。这是因为 SMP 的配置方式。在大多数情况下,Python 进程的当前工作目录或从命令行启动的“驱动程序”脚本的目录将位于 SMP 上的第一个目录。因此,即使执行绝对导入,
import this
也可能找不到标准库this
。(另一方面,import sys
会找到标准库;请参阅最后一节。)同时,标准库并没有很好地利用打包的优势。它没有自己的根包;而且大多数情况下,当标准库模块相互依赖时,它们都使用绝对导入。
最后两点可以以令人惊讶的方式相互作用:
$ 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
),其中包含实际用于加载模块的模块加载器。
文档给出了近似该过程的配方。(当然,实际操作不必解决导入的引导问题sys
,importlib.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)