从使用 pyinstaller 运行单独的 python 脚本的 python 脚本创建 .exe

Creating a .exe from a python script which runs a separate python script using pyinstaller

简短版本:

我有一系列连接在一起的 python 脚本(一个 .py 关闭,运行 一个单独的 .py)。当 运行 通过终端在 VS Code 或 cmd 行中时,这完全可以正常工作。一旦它在 pyinstaller 的 .exe 中,只有第一个代码有效并且程序在尝试执行单独的 .py 文件时关闭。

详情:

所有单独的 python 文件都保存在同一目录中。第一个打开的 'Main.py' 有一个 tkinter 界面,允许用户 select 他们想要 运行 哪个 .py 脚本。然后代码关闭 Main window 并使用 exec(open('chosen .py').read()) 打开 selected python 脚本。 (这是初始代码的简化版本,但我遇到了同样的问题)

import tkinter as tk
from tkinter import ttk
from tkinter.constants import W
from tkinter import messagebox as mb

""" Open a window to select which separate script to run"""

root = tk.Tk()
root.title('Selection Window')
root.geometry('300x200')

frame_1 = tk.LabelFrame(root, text='Choose Program')
frame_1.pack()

# Using this function to update on radio button select
def radio_button_get():
    global program_int
    choice = radio_ID.get()
    if(choice == 1):
        program_int = 1
    elif(choice == 2):
        program_int = 2

# Display confirmation popup
def run_script():
    if(program_int == 1):
        select = mb.askokcancel("Confirm", "Run choice 1?")
        if(select == 1):
            root.destroy()
        else:
            return
    if(program_int == 2):
        select = mb.askokcancel("Confirm", "No selection")
        if(select == 1):
            root.destroy()
        else:
            return

# Create radio buttons to select program 
radio_ID = tk.IntVar()
radio_ID.set(2)
program_int = 2     # Set default selection

choice_1 = tk.Radiobutton(frame_1, text='Execute Script 1', variable=radio_ID, value=1, command=radio_button_get)
choice_1.pack()
no_choice = tk.Radiobutton(frame_1, text='No Selection', variable=radio_ID, value=2, command=radio_button_get)
no_choice.pack()

# Button to run the selected code
run_button = ttk.Button(root, text='Run', command=run_script)
run_button.pack()
root.mainloop()

# Execute the other python script 
if(program_int == 1):
    exec(open('Script1.py').read())

下一个代码是 'Script1.py' 文件,在 'Main.py' 运行 末尾。这是在 VS Code 和 cmd 行中运行良好的步骤,但会导致 pyinstaller 中的 .exe 关闭。

import tkinter as tk
from tkinter import ttk

""" Create this programs GUI window"""

root = tk.Tk()
root.title('Script 1')

def run():
    root.destroy()

label = ttk.Label(root, text='Close to run')
label.pack()
button = ttk.Button(root, text='Close', command=run)
button.pack()

root.mainloop()

""" Do some code stuff here"""

# When above code is done, want to return to the Main.py window
exec(open('Main.py').read())

每个独立的.py文件之前都已经通过pyinstaller成功的变成了.exe文件。我用来执行 pyinstaller 的 cmd 行命令是 pyinstaller 'Main.py' 这成功地在 dist 文件夹中创建了一个 Main.exe 并且还包括一个构建文件夹。

我已经通读了 pyinstallers 文档,但没有找到任何我认为在这种情况下有用的内容。我能找到的最近的问题是在 .spec 文件选项中将 python 脚本作为模块导入,但由于代码将 python 脚本作为单独的实体执行,我认为这不是解决方法。

问题出在脚本的编码方式和相互引用方式上,还是出在 pyinstaller 的安装过程中?如果我在文档中遗漏了可以解释此问题的内容,请告诉我,我会查看!

非常感谢任何帮助,谢谢

我们必须避免使用.exec命令。这是 hacky 但不安全。参考:Running a Python script from another

改为使用import

# Execute the other python script 
if(program_int == 1):
    import Script1

还有这里:

# When above code is done, want to return to the Main.py window
import Main

就是这样,现在使用pyinstaller。


编辑:

为什么 .exe 文件无法执行另一个脚本,为什么 exec() 是问题所在:

根据文档:

Pyinstaller analyzes your code to discover every other module and library your script needs in order to execute. Then it collects copies of all those files – including the active Python interpreter! – and puts them with your script in a single folder, or optionally in a single executable file.

因此,当 pyinstaller 正在分析和创建 .exe 文件时,它当时只执行 exec() 函数(因此 pyinstaller 运行时不会抛出错误),pyinstaller 不会导入它或将其复制到您的 .exe。 file,然后在创建 .exe 文件后,在 运行 时抛出错误,指出不存在这样的脚本文件,因为它从未被编译到该 .exe 文件中。

因此,使用 import 实际上会将脚本作为模块导入,当执行 pyinstaller 时,现在您的 .exe 文件将不会出错。

与其将脚本作为模块导入,以便一次又一次地重新执行,不如将另一个脚本作为函数导入 Main.py

此外,不要破坏您的主根 window(因为除非您创建一个新的 window,否则您将无法再次打开它),请使用 .withdraw() 隐藏然后 .deiconify() 显示。


首先,在Script1.py:

import tkinter as tk
from tkinter import ttk

""" Create this programs GUI window"""
def script1Function(root):      #the main root window is recieved as parameter, since this function is not inside the scope of Main.py's root
    root2 = tk.Tk()             #change the name to root2 to remove any ambiguity
    root2.title('Script 1')

    def run():
        root2.destroy()         #destroy this root2 window
        root.deiconify()        #show the hidden Main root window
    label = ttk.Label(root2, text='Close to run')
    label.pack()
    button = ttk.Button(root2, text='Close', command=run)
    button.pack()

    root2.mainloop()

然后,在Main.py:

import tkinter as tk
from tkinter import ttk
from tkinter.constants import W
from tkinter import messagebox as mb
from Script1 import script1Function         #importing Script1's function


# Execute the other python script
def openScript1():    
    root.withdraw()                         #hide this root window
    script1Function(root)                   #pass root window as parameter, so that Script1 can show root again
    

""" Open a window to select which separate script to run"""

root = tk.Tk()
root.title('Selection Window')
root.geometry('300x200')

frame_1 = tk.LabelFrame(root, text='Choose Program')
frame_1.pack()

# Using this function to update on radio button select
def radio_button_get():
    global program_int
    choice = radio_ID.get()
    if(choice == 1):
        program_int = 1
    elif(choice == 2):
        program_int = 2

# Display confirmation popup
def run_script():
    global program_int                      #you forgot to make it global
    if(program_int == 1):
        select = mb.askokcancel("Confirm", "Run choice 1?")
        if(select == 1):
            openScript1()
        else:
            return
    if(program_int == 2):
        select = mb.askokcancel("Confirm", "No selection")
        if(select == 1):
            root.destroy()
        else:
            return

# Create radio buttons to select program 
radio_ID = tk.IntVar()
radio_ID.set(2)
program_int = 2     # Set default selection

choice_1 = tk.Radiobutton(frame_1, text='Execute Script 1', variable=radio_ID, value=1, command=radio_button_get)
choice_1.pack()
no_choice = tk.Radiobutton(frame_1, text='No Selection', variable=radio_ID, value=2, command=radio_button_get)
no_choice.pack()

# Button to run the selected code
run_button = ttk.Button(root, text='Run', command=run_script)
run_button.pack()
root.mainloop()