了解不一致的 cythonized 代码行为 - PyQt5 与 PySide2

Posted

技术标签:

【中文标题】了解不一致的 cythonized 代码行为 - PyQt5 与 PySide2【英文标题】:Understanding inconsistent cythonized code behavior - PyQt5 vs. PySide2 【发布时间】:2019-09-08 07:21:35 【问题描述】:

在对一些 PyQt5 代码进行 cythonizing 时,我遇到了TypeError: method() takes exactly 1 positional argument (2 given)。 奇怪的是,用 PySide2 替换 PyQt5 似乎不会导致这种行为。我希望有人能帮助我理解为什么会这样。 注意:直接从源代码运行不会导致 PyQt5 或 PySide2 出现此问题。

我正在使用 Python 3.6.8,cython 0.28.5。

我创建了一个示例应用程序来重现此行为。文件夹结构如下:

root/
|- main.py
|- setup.py
|- lib/
    |- __init__.py
    |- test.py

setup.py 执行与$ cythonize -i <filename> 相同的功能,同时允许我更改compiler_directives。实际代码可以在 cython repo here 中找到。

setup.py

import os
import tempfile
import shutil
from distutils.core import setup
from Cython.Build.Dependencies import cythonize
from multiprocessing import pool

def run_distutils(args):
    base_dir, ext_modules = args
    script_args = ['build_ext', '-i']
    cwd = os.getcwd()
    temp_dir = None
    try:
        if base_dir:
            os.chdir(base_dir)
            temp_dir = tempfile.mkdtemp(dir=base_dir)
            script_args.extend(['--build-temp', temp_dir])
            setup(
                    script_name='setup.py',
                    script_args=script_args,
                    ext_modules=ext_modules,
                )
    finally:
        if base_dir:
            os.chdir(cwd)
            if temp_dir and os.path.isdir(temp_dir):
                shutil.rmtree(temp_dir)

if __name__ == "__main__":
    ext_paths = ['lib\\test.py']
    cython_exts = cythonize(ext_paths,
                            nthreads=1,
                            compiler_directives=
                                "always_allow_keywords": True,
                            )
    try:
        process_pool = pool.Pool()
        process_pool.map_async(run_distutils, [(".", [ext]) for ext in cython_exts])
    except:
        if process_pool is not None:
            process_pool.terminate()
        raise
    finally:
        if process_pool is not None:
            process_pool.close()
            process_pool.join()

main.py 用于调用 test.py 内部的 main 来启动 UI。

test.py

import sys
from PyQt5.QtWidgets import QMainWindow, QPushButton, QApplication

def print_arg(arg):
    print(arg)

class Example(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.btn1 = QPushButton("Button 1", self)
        self.btn1.move(30, 50)
        self.btn2 = QPushButton("Button 2", self)
        self.btn2.move(150, 50)
        self.btn1.clicked.connect(self.buttonClicked)
        self.btn2.clicked.connect(self.buttonClicked)
        self.statusBar()
        self.setGeometry(300, 300, 290, 150)
        self.setWindowTitle('Event sender')
        self.show()

    def buttonClicked(self):
        sender = self.sender()
        self.statusBar().showMessage(sender.text() + ' was pressed')
        print_arg(arg=self.sender())

def main():
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

我通过从根目录执行$ python setup.pytest.pyd 创建.pyd。构建完成后,我将 test.py 移出 lib/ 以使用 $ python main.py 进行测试。

如上图构建并运行test.py时(使用PyQt5),点击任意按钮会导致:

Traceback (most recent call last):
  File "lib\test.py", line 26, in lib.test.Example.buttonClicked
    def buttonClicked(self):
TypeError: buttonClicked() takes exactly 1 positional argument (2 given)

test.py 中将 PyQt5 替换为 PySide2,构建然后运行代码,不会引发相同的 TypeError。这是我要调查的行为。

setup.py 中,将编译器指令 always_allow_keywords 更改为 False 将阻止 TypeError 发生,但会导致引发此错误(PyQt5 和 PySide 都会发生这种情况):

Traceback (most recent call last):
  File "lib\test.py", line 29, in lib.test.Example.buttonClicked
    print_arg(arg=self.sender())
TypeError: print_arg() takes no keyword arguments

如果有人能解释为什么 PyQt5 和 PySide2 的行为不同,那就太好了。

谢谢。

【问题讨论】:

【参考方案1】:

clicked 信号超载,即它有 2 个签名:clicked = pyqtSignal([], [bool]) 所以不指明将使用哪个签名会产生此类问题。所以解决方法是通过pyqtSlot来表示签名:

import sys
from PyQt5.QtWidgets import QMainWindow, QPushButton, QApplication
from PyQt5.QtCore import pyqtSlot # <--- add this line

def print_arg(arg):
    print(arg)

class Example(QMainWindow):
    # ...

    @pyqtSlot() # <--- add this line
    def buttonClicked(self):
        sender = self.sender()
        self.statusBar().showMessage(sender.text() + ' was pressed')
        print_arg(arg=self.sender())

def main():
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

而在PySide2的情况下,会扣除签名,但PyQt5希望你明确指出,否则它会检查所有可能的情况。

【讨论】:

以上是关于了解不一致的 cythonized 代码行为 - PyQt5 与 PySide2的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 cython 将 python 类公开给 c++

腌制一个类时,我在 python 中的行为与在 cython 中的行为不同

Cython:将 C 结构转换为 pythons 对象会增加引用计数

使用 Theano 后端的 Keras Flatten() 层行为不一致

Cython初窥

更新了 Loopback 中行为不一致的相关对象