【问题标题】:Issue when executing code snippet with "exec" and inheritance使用“exec”和继承执行代码片段时出现问题
【发布时间】:2021-09-08 23:04:01
【问题描述】:

我在尝试从 QPlainTextEdit 中执行字符串/文件时遇到了一些问题,这似乎是某种范围问题。发生的情况是,当代码 EXECUTABLE_STRING 从全局范围运行时,它可以正常工作。但是,当它从本地范围运行时,例如通过 AbstractPythonCodeWidget,它要么找不到要继承的对象TypeError: super(type, obj): obj must be an instance or subtype of type,要么遇到名称错误NameError: name 'Test' is not defined。根据exec(EXECUTABLE_STRING) 行在运行时是否被注释/取消注释,这会发生奇怪的变化。任何帮助将不胜感激。

import sys
from PyQt5.QtWidgets import QApplication, QPlainTextEdit
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor


app = QApplication(sys.argv)

EXECUTABLE_STRING = """
from PyQt5.QtWidgets import QLabel, QApplication
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor

class Test(QLabel):
    def __init__(self, parent=None):
        super(Test, self).__init__(parent)
        self.setText("Test")

a = Test()
a.show()
a.move(QCursor.pos())
"""

class AbstractPythonCodeWidget(QPlainTextEdit):
    def __init__(self, parent=None):
        super(AbstractPythonCodeWidget, self).__init__(parent)
        self.setPlainText(EXECUTABLE_STRING)

    def keyPressEvent(self, event):
        if event.modifiers() == Qt.ControlModifier:
            if event.key() == Qt.Key_Return:
                # this does not work
                #exec(compile(self.toPlainText(), "script", "exec"), globals(), locals())
                exec(self.toPlainText())
        return QPlainTextEdit.keyPressEvent(self, event)


w = AbstractPythonCodeWidget()
w.show()
w.move(QCursor.pos())
w.resize(512, 512)

# this works when run here, but not when run on the keypress event
# exec(EXECUTABLE_STRING)

sys.exit(app.exec_())

【问题讨论】:

  • 您是否正在尝试创建自己的测试环境,您可以在其中编写和编辑代码,然后进行动态测试?如果是这样,有这样做的策略。我不建议从应用程序内部编辑代码;相反,您可以像往常一样继续使用您的编辑器,并在您的测试应用程序中构建重新加载功能。这样做的机制already exists,很有帮助!
  • 更广泛地说——事实上,exec 是完成这项工作的正确工具的次数不为零,但它们很少而且相差甚远。始终谨慎对待,并寻找其他选择。
  • 所以,我使用exec 只是为了我知道如何使用它。应用程序的更广泛范围是在应用程序内部,用户可以创建 Python 脚本,然后可以在触发某些事件时运行这些脚本。用户的脚本可以是原始字符串,也可以是磁盘上的实际文件,具体取决于他们设置的设置。所以我不太确定实时编码示例会有多大帮助,但我会去看看。

标签: python inheritance pyqt exec


【解决方案1】:

首先,基于用户输入运行exec 可能是一个安全问题,但最重要的是通常会导致致命崩溃,除非采取大量预防措施,因为您使用的是相同的你的程序用户代码的解释器:基本上,如果用户的代码失败,你的程序就会失败,但这不是唯一的问题。

你的代码运行不正常的原因其实有点复杂,和类名的作用域有关,运行exec的时候就有点复杂了super().[1]

一个有趣的方面是,如果你删除 super 的参数(从 Python 3 开始你应该删除),程序不会引发任何错误。

但这还不够:a 是一个局部变量,它会在exec 完成后立即被垃圾回收,并且由于标签已分配给该变量,因此它将与它。

可能解决方案是使引用持久化,例如通过将其分配给self(因为self 存在于执行脚本的范围内)。这是EXECUTABLE_STRING 的一个工作示例:

from PyQt5.QtWidgets import QLabel
from PyQt5.QtGui import QCursor

class Test(QLabel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setText("Test")

self.a = Test()
self.a.show()
self.a.move(QCursor.pos())

如您所见,我们不再需要再次导入所有内容,我们只导入 QLabel,因为它没有在主脚本中导入,但其他所有内容都已经存在在范围内脚本,包括当前的 QApplication 实例。

也就是说,以上所有内容仅用于知识目的,因为您不应该使用exec运行用户代码。

例如,尝试将以下内容粘贴到文本编辑中,然后运行它:

self.document().setHtml('This should <b>NOT</b> happen!!!<br/><br/>Bye!')
self.setReadOnly(True)

from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication
QTimer.singleShot(2000, QApplication.quit)

如你所见,上面的代码不仅可以改变输入,还可以完全控制整个应用程序。

您可以通过调用一个基本上会限制执行范围的函数来防止这种情况发生:

def runCode(code):
    try:
        exec(code)
    except Exception as e:
        return e

class AbstractPythonCodeWidget(QPlainTextEdit):
    # ...
    def keyPressEvent(self, event):
        if event.modifiers() == Qt.ControlModifier:
            if event.key() == Qt.Key_Return:
                error = runCode(self.toPlainText())
                if error:
                    QMessageBox.critical(self, 'Script crash!', str(error))
        return QPlainTextEdit.keyPressEvent(self, event)

但这只是因为没有涉及self:您仍然可以在上面的示例中使用w.a = Test()

因此,如果您想在程序中运行用户制作的脚本,exec 可能不是一个可接受的解决方案,除非您采取足够的预防措施。

如果您的程序和用户脚本之间不需要直接交互,则可以使用subprocess 模块,并使用另一个 python 解释器实例运行脚本。

[1] 如果有人有有效的资源/答案可以阐明该主题,请发表评论。

【讨论】:

  • 这一切都是有道理的,但是,在这种情况下,期望用户需要能够访问主应用程序并修改主应用程序当前正在处理的数据。所以看起来exec 是要走的路,有很多预防措施(我们也可以假设用户使用此功能时要为崩溃负责)。
  • 还有一点需要注意,这是一个插件,主应用程序还在 Python 2.7 =\ 中,所以super().__init__() 的变通方法将不起作用。是否可以更详细地讨论范围界定问题?好像我都正确理解了这一切,我需要采取预防措施并将这些东西添加到范围中,或者等待应用程序升级到 Python 3+
  • @Emmuh 我可以理解,但要做到这一点不容易,不应被低估。您尝试做的事情可能很危险,并且需要大量 的努力(以及知识和经验)才能正确完成:您不能只做一个简单的exec。不幸的是,正如问题中所述,我不知道有关super() 问题的详细信息。
  • 我知道这并不容易,并且允许用户以这种级别访问应用程序会带来危险。关于这个主题需要注意的一些事情是,这是一个已经支持此功能的应用程序的插件,我只是想了解它是如何工作的,并为其添加额外的功能。在这种情况下,尝试通过用户驱动的事件/信号使脚本动态执行。
  • 如果应用程序是用 Python 编写的,您可以研究它的代码并了解它是如何工作的。这并不容易,可能需要很长时间,但也很有教育意义。
【解决方案2】:

在此处找到了一个类似的问题,该问题更深入地说明了作用域如何在 exec 中与 Globals/Locals 一起工作: globals and locals in python exec()

不想复制/粘贴整个线程,但在这篇文章中对我有用的答案是:

d = dict(locals(), **globals())
exec (code, d, d)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-01
    • 1970-01-01
    相关资源
    最近更新 更多