导入任意的Python源代码文件。(Python 3.3+)

97

我该如何在Python 3.3+中导入任意文件名的Python源代码文件(文件名可能包含任何字符,且不总是以.py结尾)?

我使用imp.load_module方法,如下所示:

>>> import imp
>>> path = '/tmp/a-b.txt'
>>> with open(path, 'U') as f:
...     mod = imp.load_module('a_b', f, path, ('.py', 'U', imp.PY_SOURCE))
...
>>> mod
<module 'a_b' from '/tmp/a-b.txt'>

在Python 3.3中,它仍然可以工作,但根据imp.load_module文档,它已被弃用:

自版本3.3起已弃用:不需要使用加载器来加载模块,find_module()也已被弃用。

imp模块的文档建议使用importlib代替:

注意:新程序应该使用importlib而不是此模块。

在Python 3.3+中,如何正确地加载任意python源文件,而不使用已弃用的imp.load_module函数?


5
我可以问一下你为什么这样做吗?我是importlib的维护者,一直在努力向人们询问为什么要使用imp.load_module()而不是直接使用import语句。您是否打算稍后按名称导入模块(例如import a_b)?您是否关心此方法不会使用任何自定义导入程序?您是否期望该模块具有完整功能(例如定义__name____loader__)? - Brett Cannon
4
@BrettCannon,一个第三方程序会定期(每小时一次)修改一个包含Python语句的文本文件(主要是像THIS='blah'这样的行)。该文件的名称不以.py结尾。我的程序读取该文件。 - falsetru
1
@BrettCannon,我不知道自定义导入器。我不在乎模块是否具有完整功能。 - falsetru
2
使用Python作为一个非常简单的数据结构格式。感谢提供的信息! - Brett Cannon
1
@BrettCannon — 我刚遇到了这样一个情况,我需要从名为版本号(例如“v1.0.2”)的目录中导入一些Python代码。虽然可能,但重命名目录是非常不可取的。最终我使用了stefan-scherfke的解决方案。 - Andrew Miner
显示剩余5条评论
5个回答

115
importlib 测试代码 中找到了解决方案。
使用 importlib.machinery.SourceFileLoader
>>> import importlib.machinery
>>> loader = importlib.machinery.SourceFileLoader('a_b', '/tmp/a-b.txt')
>>> mod = loader.load_module()
>>> mod
<module 'a_b' from '/tmp/a-b.txt'>

注意:仅适用于 Python 3.3+

更新Loader.load_module 自 Python 3.4 版本起已弃用。请改用 Loader.exec_module

>>> import types
>>> import importlib.machinery
>>> loader = importlib.machinery.SourceFileLoader('a_b', '/tmp/a-b.txt')
>>> mod = types.ModuleType(loader.name)
>>> loader.exec_module(mod)
>>> mod
<module 'a_b'>

>>> import importlib.machinery
>>> import importlib.util
>>> loader = importlib.machinery.SourceFileLoader('a_b', '/tmp/a-b.txt')
>>> spec = importlib.util.spec_from_loader(loader.name, loader)
>>> mod = importlib.util.module_from_spec(spec)
>>> loader.exec_module(mod)
>>> mod
<module 'a_b' from '/tmp/a-b.txt'>

34
如何改善答案?如果你有更好的方法来完成我想要的,那请告诉我。Downvoter。 - falsetru
4
warnings.catch_warnings提供了一个有用的提示,告诉我们load_module会被忽略。如果你使用mod = imp.load_source('a_b', '/tmp/a-b.txt')的话,会触发以下警告(使用-Wall):DeprecationWarning: imp.load_source()已被弃用;请改用importlib.machinery.SourceFileLoader(name, pathname).load_module()。请注意不要修改原文意思。 - Eryk Sun
1
@eryksun,你说得对。感谢您的评论。顺便说一下,Python 3.4(rc1)不像Python 3.3.x那样显示替代用法。 - falsetru
2
底部的第一个和第二个示例有什么区别? - Matthew D. Scholefield
1
@ihavenoidea,请另外发布一个问题,这样其他人可以回答你,而且其他用户也可以阅读答案。 - falsetru
显示剩余12条评论

45

更新至Python >= 3.8:

简短版:

>>> # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
>>> import importlib.util, sys
>>> spec = importlib.util.spec_from_file_location(modname, fname)
>>> module = importlib.util.module_from_spec(spec)
>>> sys.modules[modname] = module
>>> spec.loader.exec_module(module)

完整版本:

>>> import importlib.util
>>> import sys
>>> from pathlib import Path
>>> from typing import TYPE_CHECKING
>>> 
>>> 
>>> if TYPE_CHECKING:
...     import types
...
...
>>> def import_source_file(fname: str | Path, modname: str) -> "types.ModuleType":
...     """
...     Import a Python source file and return the loaded module.

...     Args:
...         fname: The full path to the source file.  It may container characters like `.`
...             or `-`.
...         modname: The name for the loaded module.  It may contain `.` and even characters
...             that would normally not be allowed (e.g., `-`).
...     Return:
...         The imported module

...     Raises:
...         ImportError: If the file cannot be imported (e.g, if it's not a `.py` file or if
...             it does not exist).
...         Exception: Any exception that is raised while executing the module (e.g.,
...             :exc:`SyntaxError).  These are errors made by the author of the module!
...     """
...     # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
...     spec = importlib.util.spec_from_file_location(modname, fname)
...     if spec is None:
...         raise ImportError(f"Could not load spec for module '{modname}' at: {fname}")
...     module = importlib.util.module_from_spec(spec)
...     sys.modules[modname] = module
...     try:
...         spec.loader.exec_module(module)
...     except FileNotFoundError as e:
...         raise ImportError(f"{e.strerror}: {fname}") from e
...     return module
...
>>> import_source_file(Path("/tmp/my_mod.py"), "my_mod")
<module 'my_mod' from '/tmp/my_mod.py'>

Python 3.5和3.6的原始答案

@falsetru的解决方案的简化版本:

>>> import importlib.util
>>> spec = importlib.util.spec_from_file_location('a_b', '/tmp/a-b.py')
>>> mod = importlib.util.module_from_spec(spec)
>>> spec.loader.exec_module(mod)
>>> mod
<module 'a_b' from '/tmp/a-b.txt'>

我用Python 3.5和3.6进行了测试。
根据评论,它不能使用任意文件扩展名。

4
对我而言,importlib.util.spec_from_file_location(..)返回了None,这导致接下来的importlib.util.module_from_spec(..)调用引发了异常。(请参见 https://i.imgur.com/ZjyFhif.png) - falsetru
5
importlib.util.spec_from_file_location 函数适用于已知的文件扩展名(如 .py.so 等),但不适用于其他扩展名(如 .txt...)。 - falsetru
哦,我只在Python文件中使用它,但修改了我的示例以看起来像上面的那个,并没有测试它...我已经更新了它。 - Stefan Scherfke

15
类似于@falsetru,但适用于Python 3.5+,并考虑到the importlib doc中关于使用importlib.util.module_from_spec而不是types.ModuleType的说明:

该函数[importlib.util.module_from_spec]优先于使用types.ModuleType创建新模块,因为spec用于尽可能设置模块上的许多受导入控制的属性。

我们可以通过修改importlib.machinery.SOURCE_SUFFIXES列表来仅使用importlib导入任何文件。
import importlib

importlib.machinery.SOURCE_SUFFIXES.append('') # empty string to allow any file
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# if desired: importlib.machinery.SOURCE_SUFFIXES.pop()

1
有趣的是,尽管将空字符串附加到源后缀列表的这种方法非常适用于导入重命名的Python源模块,但对于导入重命名的扩展模块来说,相应的方法却不起作用...也就是说,使用importlib.machinery.EXTENSION_SUFFIXES.append('')仍然会使importlib.util.spec_from_file_location返回None。 - mxxk
据推测,如果您指定一个加载器,importlib.util.spec_from_file_location 仍然可以处理扩展名。 - Alex Walczak
1
@mxxk 这是因为,如果你查看源代码,它只检查后缀是否存在于名为 __init__ 的文件中。 - Jon

6

importlib 辅助函数

这里提供了一个方便、即用型的辅助函数来替代 imp,并附有示例。该技术与 https://dev59.com/WmMk5IYBdhLWcg3wvQYU#19011259 相同,只是提供了更方便的函数。

main.py

#!/usr/bin/env python3

import os
import importlib

def import_path(path):
    module_name = os.path.basename(path).replace('-', '_')
    spec = importlib.util.spec_from_loader(
        module_name,
        importlib.machinery.SourceFileLoader(module_name, path)
    )
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    sys.modules[module_name] = module
    return module

notmain = import_path('not-main')
print(notmain)
print(notmain.x)

不是主函数

x = 1

运行:

python3 main.py

输出:

<module 'not_main' from 'not-main'>
1

我将-替换为_,因为我的可导入Python可执行文件没有扩展名,其中包含连字符,例如my-cmd。这不是强制性的,但会产生更好的模块名称,如my_cmd

这个模式也在文档中提到:https://docs.python.org/3.7/library/importlib.html#importing-a-source-file-directly

我最终采用了这种方法,因为升级到Python 3.7后,import imp会打印:

DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses

我不知道如何关闭它,这个问题被问到了:

相关问题:

在Python 3.7.3中测试通过。


1

在尝试了许多失败的解决方案后,这个对我有效。

def _import(func,*args):
    import os
    from importlib import util
    module_name = "my_module"
    BASE_DIR = "wanted module directory path"
    path =  os.path.join(BASE_DIR,module_name)
    spec = util.spec_from_file_location(func, path)
    mod = util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return getattr(mod,func)(*args)

调用它只需编写函数名称及其参数 _import("function",*args)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接