使用 PyInstaller 检查时出现问题;可以获取类的来源,但不能获取函数的来源

Tom*_*Tom 6 python pyinstaller inspect

我的第一篇文章在这里,请耐心等待......

我正在使用 PyInstaller 捆绑 tkinter 应用程序,并且在使用该inspect库时遇到了问题。基本上,我可以调用我编写的类的源代码,但不能调用函数。我做了这个人为的案例来证明:

我有一个像这样的文件夹结构:

experiment/
---experiment.py
---init.py
---resources/
    /---resources.py
    /---init.py
---loader/
    /---loader.py
    /---init.py
Run Code Online (Sandbox Code Playgroud)

resources.py定义一个函数一个类:

def foo(a):
    print(str(a) + ' is what you entered.')

class Bar:
    def __init__(self, b):
        self.b = b
Run Code Online (Sandbox Code Playgroud)

loader.py导入该函数和该类并定义一个函数来打印它们的源代码:

import inspect

from resources import resources

def testfunc():
    source_Bar = inspect.getsource(resources.Bar)
    print(source_Bar)

    source_foo = inspect.getsource(resources.foo)
    print(source_foo)    
Run Code Online (Sandbox Code Playgroud)

experiment.py从中加载打印函数loader.py并调用它:

from loader.loader import testfunc

testfunc()
Run Code Online (Sandbox Code Playgroud)

我可以experiment.py在 Python 控制台中运行并获得预期的输出(foo和的源代码Bar)。

然后,我使用 PyInstaller 创建可执行文件experiment.py(从仅添加 PyInstaller 的虚拟环境)。该spec文件未受影响(我可以共享它),但我确实将loaderresources目录复制/粘贴到dist/experiment,以便可执行文件可以找到它们。experiment.exe如果我从命令提示符运行,我会得到以下输出:

C:\Users\earne\Desktop\experiment\dist\experiment>experiment.exe
class Bar:
    def __init__(self, b):
        self.b = b

Traceback (most recent call last):
  File "experiment\experiment.py", line 3, in <module>
  File "experiment\loader\loader.py", line 9, in testfunc
  File "inspect.py", line 973, in getsource
  File "inspect.py", line 955, in getsourcelines
  File "inspect.py", line 786, in findsource
OSError: could not get source code
[14188] Failed to execute script experiment
Run Code Online (Sandbox Code Playgroud)

因此找到并打印了 的代码,但是找不到 的Bar代码foo!请注意,第 9 行是foo检查的地方。

真正的上下文是一个绘图程序,我想返回用于绘制绘图的代码,以防用户想要进行调整。所以foo实际上是很多matplotlib代码,loader是格式化绘图代码的模块,experimenttkinter应用程序。在本例中,inspect在 IDE 中工作正常,但在构建 exe 后就中断了。

有关我的设置的更多信息:

  • virtualenv 是使用 Anaconda Prompt 创建的;我曾经conda create新建一个环境,然后pip安装PyInstaller
  • Python 是 3.7.7
  • 环境中的一切,基本上是它所附带的:
altgraph       0.17
certifi        2020.4.5.1
future         0.18.2
pefile         2019.4.18
pip            20.0.2
pyinstaller    4.0.dev0+03d42a2a25
pywin32-ctypes 0.2.0
setuptools     46.1.3.post20200330
wheel          0.34.2
wincertstore   0.2
Run Code Online (Sandbox Code Playgroud)
  • Windows 10 64 位

我尝试过的:

  • 大量切换导入方法(例如按名称显式导入函数、使用 import *、仅导入模块并使用 module.function)
  • 弄乱该.spec文件,例如将我的模块添加datasAnalysis. 我已经看到了这个问题,并且我尝试将我的模块添加到a.purehtgoebel 评论中,但我不确定我是否做对了,而且似乎可能不是根本问题,因为该类的代码Bar可以从同一个文件中找到
  • 我使用了最新的 PyInstaller 版本 (3.6) 和当前的开发人员版本https://github.com/pyinstaller/pyinstaller/archive/develop.zip

总的来说,最奇怪的部分似乎是,当类源代码位于同一个文件中时,可以找到类源代码,但无法找到函数- 但也许 PyInstaller 正在以一种我不明白的方式使其变得更加复杂?如果您有任何建议或希望我尝试其他方法,请告诉我。很高兴提供我能提供的任何其他信息/测试。干杯!

Tom*_*Tom 2

经过一番摸索后,我得到了一个解决方案 - 但感觉并不理想。

loader添加了语句来打印两者的模块和文件fooBarmymod”是不必要的):

import inspect
from resources import resources as mymod

def testfunc():
    print(inspect.getfile(mymod.Bar))
    print(inspect.getmodule(mymod.Bar))
    source_Bar = inspect.getsource(mymod.Bar)
    print(source_Bar)

    print(inspect.getfile(mymod.foo))
    print(inspect.getmodule(mymod.foo))
    source_foo = inspect.getsource(mymod.foo)
    print(source_foo)
Run Code Online (Sandbox Code Playgroud)

再次创建程序, 的输出指向和experiment.exe之间的差异。的打印语句是:fooBarmymod.Bar

C:\Users\...\dist\experiment\resources\resources.pyc
<module 'resources.resources' from 'C:\\Users\...\dist\\experiment\\resources\\resources.pyc'>
class Bar:
    def __init__(self, b):
        self.b = b
Run Code Online (Sandbox Code Playgroud)

和对于mymod.foo

experiment\resources\resources.py
<module 'resources.resources' from 'C:\\Users\...\dist\\experiment\\resources\\resources.pyc'>
Traceback (most recent call last):
... #same error as before
Run Code Online (Sandbox Code Playgroud)

所以看起来Python可以识别它们来自同一个模块,但不能识别实际的文件;有一个指向.pyc文件的绝对路径Bar,而只有一个指向.py文件的相对路径foo

所以我尝试更改导入/更加明确,并开始使用importlib

from importlib import import_module
mymod = import_module('resources.resources')
Run Code Online (Sandbox Code Playgroud)

修复此问题experiment.exe以提供正确的输出:

C:\Users\...\dist\experiment\resources\resources.py
<module 'resources.resources' from 'C:\\Users\...\dist\\experiment\\resources\\resources.py'>
class Bar:
    def __init__(self, b):
        self.b = b

C:\Users\...\experiment\resources\resources.py
<module 'resources.resources' from 'C:\\Users\...\dist\\experiment\\resources\\resources.py'>
def foo(a):
    print(str(a) + ' is what you entered.')
Run Code Online (Sandbox Code Playgroud)

因此,即使如此,我也不明白为什么需要更改导入。此外,此修复在我的 GUI 上下文中不起作用 - 我仍然无法获取函数源代码,但该类继续工作。我最终不得不遵循这个例子并执行以下操作:

import importlib.util
homedir = os.path.dirname(os.path.dirname(__file__)
resources_dir = os.path.join(homedir, 'resources/resources.py')
spec = importlib.util.spec_from_file_location("resources.resources", resources_dir)
mymod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mymod)
#I do os stuff to get a relative path to resources from loader
Run Code Online (Sandbox Code Playgroud)

这在应用程序中有效 - 看起来过于冗长和混乱,但最终它不会明显影响应用程序的功能,无论是作为还是.exe作为 Python 脚本。

如果有人想大声喊叫,我仍然很想知道这里发生了什么。希望这对某人有帮助(如果没有的话)。