在Python 3中,execfile的替代方法是什么?

450

看起来他们在Python 3中取消了所有快速加载脚本的简单方式,通过移除execfile()

我是否遗漏了一些明显的替代方案?


2
reload已经回归,作为3.2版本之后的imp.reload - Danica
20
如果您正在交互式地使用Python,请考虑使用IPython:%run script_name适用于所有版本的Python。 - Michael
3
自3.4版本起,imp模块被替换为必须先导入的importlib模块。使用importlib.reload(mod_name)可以重新导入并执行mod_name模块。 - P. Wormer
3
runfile("filename.py") 有什么问题? - mousomer
1
谢谢 @mousomer!! 我正需要 runfile() 的功能,因为我需要运行一个在其自己的命名空间中执行的 Python 脚本(与在 调用 命名空间中执行相反)。 我的应用程序:使用 __file__ 属性将被调用脚本的目录添加到系统路径 (sys.path) 中:如果我们使用 execfile() 或其 Python 3 中的等效方法 (exec(open('file.py').read())),则包含的脚本将在调用命名空间中运行,因此 __file__ 解析为 调用 文件名。 - mastropi
@TarmoPikaro 撤销了您的编辑,修改 sys.argv 真的超出了此函数的范围,并且有一些(在我看来)不可接受的缺点,因为调用者可能在其他地方使用 sys.argv。这可能会被暂时覆盖(这不是线程安全的)...但在某些情况下仍然可以接受。无论如何 - 这不是原始 execfile 支持的,如果您想要可选地传递环境、argv、工作目录等,则可以提供一个单独的答案。 - ideasman42
12个回答

489

61
他们为什么要这样做呢?这比以前冗长得多。而且,在Python3.3上它对我无效。当我执行(open('./some_file').read())时,会出现“没有这样的文件或目录”的错误。我已经尝试包括'.py'扩展名,也尝试过去掉'./'。 - JoeyC
34
不那么琐碎地说,这不像execfile()一样在引发异常时提供行号信息。 - KDN
41
你需要关闭那个文件句柄。这也是不喜欢从Python 2改变的另一个原因。 - Rebs
6
在那个例子中,你不需要关闭文件句柄,它会自动关闭(至少在普通的CPython中)。 - tiho
8
在CPython中,对象的引用计数归零后会被垃圾回收,只有循环引用可能会延迟这一过程(参考https://dev59.com/jWox5IYBdhLWcg3wFAY1)。在这种情况下,应该会在read()返回后立即进行。文件对象在删除时会关闭(注意:我意识到此链接明确表示“始终关闭文件”,这确实是一般应遵循的良好实践)。 - tiho
显示剩余12条评论

245

您只需要阅读文件并自己执行代码。2to3当前替换

execfile("somefile.py", global_vars, local_vars)

作为

with open("somefile.py") as f:
    code = compile(f.read(), "somefile.py", 'exec')
    exec(code, global_vars, local_vars)

(编译调用不是必需的,但它将文件名与代码对象关联起来,使调试变得更容易。)
请参见:

3
这对我有效。但是,我注意到你把本地和全局参数的顺序写反了。实际上应该是:exec(object[, globals[, locals]])。当然,如果你在原始代码中顺序颠倒了参数,那么2to3将会生成与你所说的完全相同的结果。 :) - Nathan Shively-Sanders
4
很高兴发现,如果你忽略全局变量和局部变量,这里的Python3替代方案也适用于Python2。尽管在Python2中exec是一个语句,但exec(code)也可以工作,因为括号会被忽略。 - medmunds
2
使用编译器可以解决问题。我的“somefile.py”文件包含了“inspect.getsourcefile(lambda _: None)”代码,由于“inspect”模块无法确定代码的来源,所以没有编译会导致失败。 - ArtOfWarfare
19
这真的很丑陋。你知道为什么他们在3.x中放弃了execfile()吗?execfile还可以轻松传递命令行参数。 - aneccodeal
3
如果somefile.py使用的字符编码不同于locale.getpreferredencoding(),则open("somefile.py")可能是不正确的。可以使用tokenize.open()代替。 - jfs
另一个注意事项:在Python 2中,如果源代码存在尾随空格或使用除 '\n' 以外的行结束符,则 compile() 将失败。 - itsadok

98

虽然 exec(open("filename").read()) 经常被提供作为 execfile("filename") 的替代方案,但它缺少了 execfile支持的重要细节。

下面的函数适用于Python3.x,尽量接近直接执行文件的行为。这与运行 python /path/to/somefile.py 相匹配。

def execfile(filepath, globals=None, locals=None):
    if globals is None:
        globals = {}
    globals.update({
        "__file__": filepath,
        "__name__": "__main__",
    })
    with open(filepath, 'rb') as file:
        exec(compile(file.read(), filepath, 'exec'), globals, locals)

# Execute the file.
execfile("/path/to/somefile.py")

注意:

  • 使用二进制文件读取来避免编码问题。

  • 保证关闭文件 (Python3.x 对此发出警告)。

  • 定义了__main__, 有些脚本依赖于它来检查它们是作为模块加载还是直接运行,例如:if __name__ == "__main__"

  • 设置__file__对于异常消息更好,并且一些脚本使用__file__来获取相对于它们的其他文件的路径。

  • 接受可选的全局和局部参数,在原地修改它们,就像execfile所做的那样 - 因此您可以在运行后通过读回变量来访问任何定义的变量。

  • 与Python2的execfile不同,这不会默认修改当前命名空间。要实现这一点,必须显式传递globals()locals()


82

最近在python-dev邮件列表中提到,建议使用runpy模块作为一种可行的替代方案。引用该消息:

https://docs.python.org/3/library/runpy.html#runpy.run_path

import runpy
file_globals = runpy.run_path("file.py")

execfile有微妙的差别:

  • run_path总是创建新的命名空间。它把代码作为一个模块来执行,因此全局变量和局部变量之间没有区别(这就是为什么只有一个init_globals参数)。全局变量将被返回。

    execfile在当前命名空间或给定的命名空间中执行。如果给定了localsglobals的语义类似于类定义内部的局部变量和全局变量。

  • run_path不仅可以执行文件,还可以执行egg和目录(有关详细信息,请参阅其文档)。


1
由于某种原因,它在屏幕上输出了许多未被要求打印的信息(例如Anaconda Python 3中的'builtins'等)。有没有办法关闭它,以便只有使用print()输出的信息可视化? - John Donn
是否也可以获取当前工作区中的所有变量,而不是它们都存储在 file_globals 中?这样可以避免为每个变量键入 file_globals['...'] - Adriaan
1
此外,在 runpy 函数返回后,由执行的代码定义的任何函数和类都不能保证能正常工作。值得注意的是,这取决于您的使用情况。 - nodakai
@Adriaan 执行 "globals().update(file_globals)"。个人而言,我最喜欢这种解决方案,因为在决定更新当前工作区之前,我可能会捕捉到错误。 - Ron Kaminsky
@nodakai 感谢提供的信息,我之前没有遇到过这样的问题,不知道是什么原因导致的。 - Ron Kaminsky

25

这个更好,因为它从调用者那里获取全局和局部的变量:

import sys
def execfile(filename, globals=None, locals=None):
    if globals is None:
        globals = sys._getframe(1).f_globals
    if locals is None:
        locals = sys._getframe(1).f_locals
    with open(filename, "r") as fh:
        exec(fh.read()+"\n", globals, locals)

实际上,这个更接近于py2的execfile。即使在使用其他上面发布的解决方案失败时,它也适用于我的pytests。谢谢! :) - Boriel

17

你可以编写自己的函数:

def xfile(afile, globalz=None, localz=None):
    with open(afile, "r") as fh:
        exec(fh.read(), globalz, localz)

如果您确实需要...


2
-1:exec语句不能这样使用。代码在任何Python版本中都无法运行。 - nosklo
6
默认参数值在函数定义时被评估,使得globalslocals都指向包含execfile()定义的模块的全局命名空间,而不是调用者的全局和本地命名空间。正确的方法是使用None作为默认值,并通过inspect模块的内省能力确定调用者的全局和本地命名空间。 - Sven Marnach

15

如果你想要加载的脚本与你运行的脚本在同一个目录中,也许"import"就可以完成工作了?

如果你需要动态导入代码,则值得查看内置函数__import__和模块imp

>>> import sys
>>> sys.path = ['/path/to/script'] + sys.path
>>> __import__('test')
<module 'test' from '/path/to/script/test.pyc'>
>>> __import__('test').run()
'Hello world!'

test.py:

def run():
        return "Hello world!"

如果您正在使用Python 3.1或更高版本,您还应该查看importlib

这对我来说是正确的答案。这篇博客在解释importlib方面做得很好 https://dev.to/0xcrypto/dynamic-importing-stuff-in-python--1805 - Nick Brady
2
这个链接已经失效了,因为我删除了我的dev.to账户。重新发布在https://hackberry.xyz/dynamic-importing-stuff-in-python。 - 0xcrypto

11

以下是我所拥有的(在这两个例子中,file 已经被指定为包含源代码文件的路径):

execfile(file)

我替换成了以下内容:

exec(compile(open(file).read(), file, 'exec'))

我最喜欢的部分是,第二个版本在 Python 2 和 3 中都能正常工作,这意味着不需要添加与版本相关的逻辑。


8
避免使用exec()。对于大多数应用程序而言,更好的方式是利用Python的导入系统。
这个函数使用内置的importlib将一个文件当作一个实际的模块来执行:
from importlib import util

def load_file_as_module(name, location):
    spec = util.spec_from_file_location(name, location)
    module = util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module

使用示例

我们有一个名为foo.py的文件:

def hello():
    return 'hi from module!'
print('imported from', __file__, 'as', __name__)

将其作为常规模块导入:

>>> mod = load_file_as_module('mymodule', './foo.py')
imported from /tmp/foo.py as mymodule
>>> mod.hello()
hi from module!
>>> type(mod)
<class 'module'>

优点

这种方法不会污染命名空间或干扰您的 $PATH,而 exec() 直接在当前函数的上下文中运行代码,可能会导致名称冲突。此外,像 __file____name__ 这样的模块属性将被正确设置,并且代码位置将得到保留。因此,如果您已经附加了调试器或者如果该模块引发异常,您将获得可用的跟踪信息。

请注意,与静态导入的一个小差别是每次运行 load_file_as_module() 时都会导入(执行)该模块,而不仅仅是一次性使用 import 关键字。


1
很棒的答案!您可以通过说明调用两次相同文件的load_file函数是否会重新加载文件来扩展它(如果没有尝试,我就不知道)。 - gerrit
1
@gerrit 谢谢!我添加了一条注释。(正如 loader.exec_module() 的名称所暗示的那样,它在每次调用时都会被(重新)执行。) - Arminius

6
请注意,如果您使用的是不是ascii或utf-8的PEP-263编码声明,则上述模式将失败。您需要找到数据的编码并正确地进行编码,然后再将其传递给exec()函数。
class python3Execfile(object):
    def _get_file_encoding(self, filename):
        with open(filename, 'rb') as fp:
            try:
                return tokenize.detect_encoding(fp.readline)[0]
            except SyntaxError:
                return "utf-8"

    def my_execfile(filename):
        globals['__file__'] = filename
        with open(filename, 'r', encoding=self._get_file_encoding(filename)) as fp:
            contents = fp.read()
        if not contents.endswith("\n"):
            # http://bugs.python.org/issue10204
            contents += "\n"
        exec(contents, globals, globals)

4
“the above pattern” 是什么?请在提到 StackOverflow 上的其他帖子时使用链接。相对定位术语,如“上面的”,不适用,因为有三种不同的排序回答的方式(按投票、按日期或按活动),而最常见的一种(按投票)是不稳定的。随着时间的推移,您的帖子及其周围的帖子将具有不同的分数,这意味着它们将被重新排列,此类比较将变得不太有用。 - ArtOfWarfare
非常好的观点。鉴于我写下这个答案已经将近六个月了,我认为“上述模式”指的是https://dev59.com/YHRC5IYBdhLWcg3wAcbU#2849077(不幸的是你必须点击才能解决),或者更好的是Noam的答案: - Eric
2
通常情况下,当我想在我的回答中引用同一问题的其他答案时,我会打上“诺姆的回答”(例如),并将文本链接到我所引用的答案,以防答案在未来与用户解除关联,例如因为用户更改了其帐户名称或该帖子变成了公共维基,因为对其进行了太多编辑。 - ArtOfWarfare
如何获取帖子中特定“答案”的URL,但不包括答案发布者的名称? - DevPlayer
查看源代码并获取ID。例如,您的问题将是https://dev59.com/YHRC5IYBdhLWcg3wAcbU#5643233?noredirect=1#comment47190856_5643233#comment-47190856。我完全赞成更好的方法,但当我在评论附近悬停时没有看到任何东西。 - Eric
显示剩余4条评论

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