PyQt5桌面应用开发(8):从QInputDialog转进到函数参数传递

PyQt5桌面应用系列

How old are you, Dialog?

兜兜转转,觉得Dialog这个话题还有一点点可以写一篇。那就是QIputDialog。

我本人是不知道为啥要有这个类的。

因为我确实没感觉到有太大的需要,UI提供了在位的输入元素,比如QLineEdit、Spinner、Slider之类,直接输入就行,跳出一个对话框,让用户输入一个简单的文本、数字、浮点类型,到底有什么必要。

从用户体验上看,惊喜是应该尽可能少出现的,比如弹出一个对话框。我看到清华出版的那本《PyQt从入门到精通》里面关于QInputDialog的例子,点击一个文本框,弹出一个对话框,输入一个文本,点Ok关闭对话框,文本加入文本框。实在是叹为观止,惊为天人……

那么为什么我也写一个篇呢?有好几个理由。

  1. 我想找一个用它的理由;
  2. 我实在搬砖搬到“让用户体验毁灭吧!”
  3. 居然觉得这是一个搞清楚闭包的机会……【!】

QInputDialog minimalist

下面我们做一个最小化的QInputDialog的例子。

QInputDialog
其实还不错!

报表:

  • 用户选择的整数显示在一个QLabel上;
  • 用户选的数据可以打印出来

数据:

  • 一个整数, ∈ [ 0 , 100 ] \in [0, 100] [0,100]
  • 通过QInputDialog.getInt获得

所以这个最小化的版本里面没有按照对象继承的方法,基本的流程采用面向过程的方式编写。

import sys
from functools import partial
from types import SimpleNamespace

from PyQt5.QtWidgets import QApplication, QInputDialog, QMainWindow, QPushButton, QLabel, QWidget, QVBoxLayout


def global_gis(parent: QWidget, text_output: QLabel, numbers: SimpleNamespace, _: bool):
    val, flag = QInputDialog.getInt(parent, "Any number in 0,100", "Number", 50, 0, 100)
    if flag:
        text_output.setText(f"Number: {val}")
        numbers.n = val
    else:
        text_output.setText(f"Number not set")


if __name__ == '__main__':
    app = QApplication(sys.argv)

    wm = QMainWindow()

    but = QPushButton("Get an Integer")
    label = QLabel("Number: ")
    cw = QWidget(wm)
    box = QVBoxLayout(cw)
    cw.setLayout(box)

    ret = SimpleNamespace()

    but.clicked.connect(partial(global_gis, cw, label, ret))
    # but.clicked.connect(lambda check: global_gis(cw, label, ret, check))

    but2 = QPushButton("Current n")
    but2.clicked.connect(lambda check: print(ret.n))

    box.addWidget(but)
    box.addWidget(label)
    box.addWidget(but2)
    wm.setCentralWidget(cw)

    print(id(cw), id(label))

    # cw = None
    # label = None

    wm.setMinimumSize(400, 30)
    wm.show()

    sys.exit(app.exec_())

这里唯一麻烦事情就是,我不想定义一个类来继承QWidget,出来数据的是一个函数global_gis,这就带来一些麻烦。因为PyQt5的槽函数的形式都是类似于def slot_func(check: bool)->None的形式,那么要完成我们的功能就需要一个函数完成两个功能:

  • 类似于C/C++/C#的引用调用的方式,在参数里把QInputDialog获得的数字传递出来;
  • 还需要把QLabel或者这里的父节点传递进去。

最终,这里选择实现一个完整的函数:def global_gis(parent: QWidget, text_output: QLabel, numbers: SimpleNamespace, _: bool),然后采用partial函数,把这个函数包装成QPushButton.clicked的槽函数的形式,只有一个参数。

why not lambda

上面的程序中注释的解释了为什么不采用lambda,如果按照这种定义方式,如果在程序的下方,cw和label发生了改变,那么就会引起程序直接退出。

but.clicked.connect(lambda check: global_gis(cw, label, ret, check))
# ......
cw = None
label = None

这里的问题就在与Python的函数调用方式。Python的参数传递方式并不是传值,也不是传引用。Python实现了一个非常独特的函数调用。函数的参数实际上是采用赋值的方式传递的,通过赋值,在函数的locals()中保存对应的对象引用。这一点可以通过id()函数来查看函数参数的地址,实参与函数中的形参完全一致。

def compare_ids(x, x_id):
    print(id(x), " == ", x_id)

x = 10  # anything
compare_ids(x, id(x))
print(id(x), " == ", id(10))

把这个值设置成任何值,都会发现,传递进去的对象是一致的。这应该是出于性能的考虑,类似于传递引用的方式。这里就很好的展示了Python中变量名和对象的关系。变量名 ↦ \mapsto 对象,变量名的类型可以随意改变,但是对象有其类型。这两个是不同的。

在函数的内部,访问一个函数参数的值,这没有什么特别的,函数传进来一个对象,函数参数是一个变量名,这个变量名在这个范围内(locals())指向这个对象。

但是在对这个变量名进行赋值的过程时,发生的情况就是这个变量指向的对象发生了改变(这是赋值在Python中的语义)。所以在函数中,改变函数参数的值,并不会改变实际参数(传进来的那个对象)的值。

这是第一个问题:函数参数的传递,Python传的是引用,传进去后绑定到局部变量名。上面的lambda还有另外一个问题。

lambda check: global_gis(cw, label, check)相当于

def _(check: bool):
    global_gis(cw, label, check)

这里匿名函数的内部访问了两个值:cw和label,这两个值在局部变量中没有,那么这种情况下Python会怎么去找变量名所绑定的对象呢?

  1. 在变量最近的scope中找;
  2. 在包含函数定义的scope中找。

所以,这两个变量就变成了main块中cw和labe。但是,Python变量的绑定是发生在运行时的,所以只有这个函数global_gis实际被调用时,才会找到这两个变量对应的对象,把它们赋值给函数的形式参数。如果,在运行这个函数之前,这两个变量的指向发生了改变,oops!

这就是为什么这里不能这样做。

上面这些分析,给我们通过槽函数来传递参数出来指明了路径。找一个对象,这个对象的内部状态会发生改变,把这个对象传递进函数,在函数内部改变其状态。这里我们用一个简单的SimpleNamespace对象,其实上用list、dict都行。

明白Python的函数变量名和对象的绑定关系,以及函数的参数传引用+赋值的调用方式,我们要做的就很简单,就在调用clicked.connect的当时把相应的对象传递进去,也就是强制对象绑定在当时发生。这就需要用函数编程中的partial出场了。

and how partial works

functools.partial是Python自带的一个返回函数的函数。其实现类似于:

def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

在执行这个函数时,第一个参数就是被包装的函数,根据调用它输入的参数,它将被调用函数的部分或全部参数固定下来,构成一个新的函数,这个新函数的参数就是哪些没固定的参数。

这里很清楚的是,因为partial是一个函数,那么在connect调用的过程中,它会被实际执行,此时其参数就会被实参绑定。那么在调用这个函数后,变更变量cw和label指向的对象,就再也不影响global_gis内部的变量绑定。

Summary

  1. Python的函数参数传递方式:传引用+赋值调用;
  2. Python的变量绑定时运行时发生的
  3. QInputDialog真的没啥用……
posted @ 2023-05-05 17:30  大福是小强  阅读(33)  评论(0编辑  收藏  举报  来源