Python3 重载主模块(__main__)的权宜之计(workaround)

本文经验仅供参考。

概要

既不了解 import 原理,也不了解 ModuleSpec, loader, importer ,更不了解 importlib 模块的情况下,所写出的邪门歪道式强行重载主模块(__main__)的方法。

别名/Alias for SEO

重载 / 重新加载 / 热重载 / 热更新
主模块 / 当前模块 / 顶层模块 / 顶层脚本环境 / 顶层代码 / 顶层文件 / 主文件
权宜之计 / 解决方法 / 怎么 / 怎样 / 如何
交互模式 / 交互式 / 互动式
Python3 / Python 3.x / py3
top-level script, interactive mode, main module reload, workaround, how to

权宜之计/TL;DR

示例:

1
2
3
4
5
6
7
8
9
10
11
12
def reload__main__():
'''reload __main__ workaround'''
# __main__ module
mod = sys.modules['__main__']

# reload
loader = mod.__dict__['__loader__']
loader.exec_module(mod)

# from __main__ import *
sys.modules['__main__'].__dict__.update(mod.__dict__)
return mod

或者这篇讨论中列举的方法?(未验证)

https://stackoverflow.com/questions/29960634/reload-the-currently-running-python-script/29962256#29962256

常见问题/FAQ

  • 方法可靠吗?

    不可靠,这是未经任何正式文档记录的旁门左道

  • 适用情况?

    适用于 python -i 交互模式下对当前主模块脚本文件(__main__)整个模块的重载。
    未必适用于其他交互式/REPL情况,例如 python -m idlelib -r IDLE 下用上述方法重载主模块会失败

  • 会影响已加载模块吗?

    不会顺便重载其他模块,也不会影响已经加载的旧模块对象。

  • 为什么示例代码运行后没有效果

    视乎 Python3 具体版本的不同,方法未必适用。(你环境有问题吧?)

  • 有什么副作用?

    重载当前主模块必然会导致全局命名空间的混乱,因此应当谨慎使用。

  • 对任何 Python3 脚本文件都适用吗?

    单文件模块视乎其文件名未必能进行 import 操作,如含有.(点分隔符)的情况。

  • 如何重载其他模块?

    如果只需要重载模块,可以使用 importlib.reload(),在此不再赘述。

  • 其他交互式/REPL情况要如何重载主模块?

    不清楚,也许可以尝试获取主模块路径,从路径导入并重载模块再覆盖全局变量的办法。
    (不适用的原因也许是因为“主模块”不是真正的“顶层模块(__main__)”?)
    (用回 python -i
    ipython %autoreload 黑魔法)

目录

  • 概要
    • 别名/Alias for SEO
  • 权宜之计/TL;DR
  • 常见问题/FAQ
  • 目录
  • 解决过程
    • 过程目标
  • 具体说明
    • 原理
    • 具体实现
    • 相关
  • 参考

解决过程

直接原因:以 python -i 交互模式调试时方便重载。
环境:Win10

过程目标

  • 重载当前主模块(__main__)

    近似于 from ... import * 的对应 reload() 方式。

  • 重载模块名中含有.(点分隔符)的模块

    直接使用 reload() 源码进行邪门歪道式强行重载。
    (命名不规范,调试两行泪)

  • (获取当前主模块文件路径)
    • __file__
    • sys.argv[0]
    • sys.modules['__main__'].__dict__['__loader__'].__dict__['path']

具体说明

原理

参考相关文档及源码,在此不赘述:

具体实现

关键在于如何获取重载当前主模块。
除了上文示例所述,具体还有几种实现方法,但可能效果上各自有些差异。(重载有四种写法)

代码全凭印象,未经严格验证,实际效果可能有出入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import sys, os, importlib
'''try to reload __main__'''

def _reload_1(path=None):
# path = path or sys.argv[0]
file = os.path.basename(path)
name = os.path.splitext(file)[0]
path = os.path.abspath(path)

spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod

def _reload_2(mod=None):
# mod = mod or sys.modules['__main__']
spec = mod.__spec__
spec.loader.exec_module(mod)
return mod

def _reload_3(mod=None):
# mod = mod or sys.modules['__main__']
loader = mod.__dict__['__loader__']
loader.exec_module(mod)
return mod

# # fail
# def _reload_4(path=None):
# # path = sys.modules['__main__'].__dict__['__loader__'].__dict__['path']
# path = path or sys.argv[0]
# path = os.path.abspath(path)
# spec = importlib.machinery.PathFinder.find_spec(path)
# mod = importlib.util.module_from_spec(spec)

# spec.loader.exec_module(mod)
# return mod

from importlib import _bootstrap
def _reload_5(mod=None):
# mod = mod or sys.modules['__main__']
sys.modules[mod.__name__] = mod
spec = mod.__spec__
_bootstrap._exec(spec, mod)
return mod

# from importlib import reload
# import _another_module_ as mod
# def _reload_6():
# global mod
# name = mod.__name__
# _globals = sys.modules['__main__']

# mod = __import__(name, _globals, locals(), [], 0)
# reload(mod)
# return mod

相关

参考

0%