编译脚本后如何在 python 中重新加载模块?

How to make a module reload in python after the script is compiled?

涉及的基本思想:

我正在尝试制作一个学生可以编写代码的应用程序 与特定问题相关(比如检查数字是否为偶数) 然后由应用程序检查学生给出的代码 将用户代码给出的输出与正确的输出进行比较 由应用程序中已存在的正确代码给出。

我正在做的项目的基础版本:

一个应用程序,您可以在其中编写 python 脚本(在 tkinter 文本中 盒子)。文本框的内容首先存储在一个test_it.py 文件。然后这个文件被导入(点击一个按钮) 应用。然后调用 test_it.py 中的函数 获取代码的输出(由用户)。

问题:

由于我正在“导入”test_it.py 的内容,因此, 在应用程序的 运行 时间内,用户可以测试他的脚本 只有一次。原因是 python 将导入 test_it.py 只归档一次。因此,即使在将用户的新脚本保存在 test_it.py ,应用程序将无法使用它。

解决方法:

Reload test_it.py 每次单击测试脚本的按钮时。

实际问题:

虽然当我从脚本 运行 应用程序时这完美地工作, 此方法不适用于文件的 compiled/executable 版本 (.exe)(这是预期的,因为在编译期间所有导入的模块将被 编译过所以以后修改它们将不起作用)

问题:

我希望我的 test_it.py 文件即使在编译应用程序后也能重新加载。


如果您想查看应用程序的工作版本,请自行测试。你会发现它 here.

问题总结:

一个 test_it.py 程序是 运行 并且有一个可用的谓词,例如is_odd()。 每隔几分钟,就会有一个包含修改后的 is_odd() 谓词的新写入文件可用, test_it 希望将测试向量提供给修改后的谓词。

有几个简单的解决方案。

  1. 根本不要在当前进程中加载​​谓词。序列化测试向量,将其发送到计算和序列化结果的 newly forked child,并检查这些结果。
  2. 通常 eval 是邪恶的,但在这里你可能想要那个,或者执行。
  3. 用新初始化的解释器替换当前进程:https://docs.python.org/3/library/os.html#os.execl
  4. 走内存泄漏路线。使用计数器为每个新文件分配一个唯一的模块名称,操纵源文件进行匹配,并加载 that。作为奖励,这可以很容易地将当前结果与以前的结果进行比较。
  5. Reload: from importlib import reload

关键是检查程序是否运行宁为exe并将exe路径添加到sys.path.

文件program.py:

import time
import sys
import os
import msvcrt
import importlib

if getattr(sys, 'frozen', False):
    # This is .exe so we change current working dir
    # to the exe file directory:
    app_path = os.path.dirname(sys.executable)
    print('    Add .exe path to sys.path: ' + app_path)
    sys.path.append(app_path)
    os.chdir(app_path)

test_it = importlib.import_module('test_it')

def main():
    global test_it
    try:
        print('    Start')
        while True:
            if not msvcrt.kbhit(): continue
            key = msvcrt.getch()
            if key in b'rR':
                print('    Reload module')
                del sys.modules['test_it']
                del test_it
                test_it = importlib.import_module('test_it')
            elif key in b'tT':
                print('    Run test')
                test_it.test_func()
            time.sleep(0.001)
    except KeyboardInterrupt:
        print('    Exit')

if __name__ == '__main__': main()

文件test_it.py:

def test_func():
    print('Hi')

创建 .exe 文件:

pyinstaller --onefile  --clean program.py

复制_text_it.py到_dist_文件夹即可。 在程序window中按t到运行test_func。编辑 test_it.py 然后按 r 重新加载模块并再次按 t 查看更改。

也许解决方案是使用 code module:

import code
# get source from file as a string
src_code = ''.join(open('test_it.py').readlines())
# compile the source
compiled_code = code.compile_command(source=src_code, symbol='exec')
# run the code
eval(compiled_code) # or exec(compiled_code)

即使是捆绑应用程序导入也以标准方式工作。这意味着每当遇到 import 时,解释器都会尝试找到相应的模块。您可以通过将包含目录附加到 sys.path 使您的 test_it.py 模块可被发现。 import test_it 应该是动态的,例如在一个函数中,这样它就不会被 PyInstaller 发现(这样 PyInstaller 就不会尝试将它与应用程序捆绑在一起)。

考虑以下示例脚本,其中应用程序数据存储在托管 test_it.py 模块的临时目录中:

import importlib
import os
import sys
import tempfile

def main():
    with tempfile.TemporaryDirectory() as td:
        f_name = os.path.join(td, 'test_it.py')

        with open(f_name, 'w') as fh:  # write the code
            fh.write('foo = 1')

        sys.path.append(td)  # make available for import
        import test_it
        print(f'{test_it.foo=}')

        with open(f_name, 'w') as fh:  # update the code
            fh.write('foo = 2')

        importlib.reload(test_it)
        print(f'{test_it.foo=}')

main()