本文目录
- PyQt5桌面应用系列
- How old are you, Dialog?
- QInputDialog minimalist
- why not lambda
- and how partial works
- Summary
PyQt5桌面应用系列
- PyQt5桌面应用开发(1):需求分析
- PyQt5桌面应用开发(2):事件循环
- PyQt5桌面应用开发(3):并行设计
- PyQt5桌面应用开发(4):界面设计
- PyQt5桌面应用开发(5):对话框
- PyQt5桌面应用开发(6):文件对话框
- PyQt5桌面应用开发(7):文本编辑+语法高亮与行号
- PyQt5桌面应用开发(8):从QInputDialog转进到函数参数传递
How old are you, Dialog?
兜兜转转,觉得Dialog这个话题还有一点点可以写一篇。那就是QIputDialog。
我本人是不知道为啥要有这个类的。
因为我确实没感觉到有太大的需要,UI提供了在位的输入元素,比如QLineEdit、Spinner、Slider之类,直接输入就行,跳出一个对话框,让用户输入一个简单的文本、数字、浮点类型,到底有什么必要。
从用户体验上看,惊喜是应该尽可能少出现的,比如弹出一个对话框。我看到清华出版的那本《PyQt从入门到精通》里面关于QInputDialog的例子,点击一个文本框,弹出一个对话框,输入一个文本,点Ok关闭对话框,文本加入文本框。实在是叹为观止,惊为天人……
那么为什么我也写一个篇呢?有好几个理由。
- 我想找一个用它的理由;
- 我实在搬砖搬到“让用户体验毁灭吧!”
- 居然觉得这是一个搞清楚闭包的机会……【!】
QInputDialog minimalist
下面我们做一个最小化的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会怎么去找变量名所绑定的对象呢?
- 在变量最近的scope中找;
- 在包含函数定义的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
- Python的函数参数传递方式:传引用+赋值调用;
- Python的变量绑定时运行时发生的
- QInputDialog真的没啥用……