前言
回调函数(callback)是一个常用的概念,最近在写代码的过程中遇到了相关问题,写个文梳理一下。以下都是个人的浅显理解,如有错误请指正。
什么是回调函数?
回调函数,本身实际上就是普通的功能函数,当功能函数以参数的方式被传入到其他函数中时,它便成为了回调函数。
下面我们将回调过程中的函数分为两部分,调用者称为调用函数,被调用者称为回调函数。
为什么要使用回调函数?
回调函数首先的作用就是解耦,调用函数不用关心回调函数的具体实现,只是按照其格式拿来即用就可以了。
回调函数的一个直观作用就是可以将事件与函数绑定,当事件发生时触发回调函数。(也是目前我遇到的场景)
关于其他的作用,我认为这篇文章讲的还不错,可以看一下学习学习。
关于同步回调和异步回调,这篇文章可以看看。
举例
应用场景是这样的:在编写一个游戏时,需要通过鼠标点击组件来使用人物背包中的物品。
其中背包组件类的实现大致如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class InventoryView(tk.Frame):
def __init__(self, master: Union[tk.Tk, tk.Frame], **kwargs) -> None:
super().__init__(master)
self.callback = None
def set_click_callback(self, callback: Callable[[str], None]) -> None:
"""
Sets the function to be called when an item is clicked.
The provided callback function should take one argument: the string name of the item.
"""
self.callback = callback
def _draw_item(self, name: str, num: int, colour: str) -> None:
"""
Creates and binds (if a callback exists) a single tk.Label in the InventoryView frame.
name is the name of the item, num is the quantity currently in the users inventory,
and colour is the background colour for this item label (determined by the type of item).
"""
label = tk.Label(self, text=f"{name}: {str(num)}", background=colour)
label.bind("<Button-1>", self.callback)
label.pack(side='top', fill=tk.X)
def draw_inventory(self, inventory: Inventory) -> None:
...
类中的callback为回调函数(这里也就是”物品使用函数“),通过与label组件上的鼠标左键点击事件绑定,实现点击调用“物品使用函数”。 可以看出,这里背包并不需要关心物品是如何被使用的,他只是接收一个回调函数,并将这个函数与背包中的每一个物品(label)绑定。
但这里其实是简化过的版本,真正在使用时,由于“物品使用函数”需要通过接收一个str类型的参数来分辨被使用的物品是何类型,所以不能直接绑定(tkinter的bind方法会将事件作为参数传入到绑定的回调函数中,这个参数表与我们规定的“物品使用函数”参数表不同)。
所以对于想要使用Event的同时绑定带参函数的情况,需要中间函数来匹配两边的规则(“物品使用函数”和bind函数)。如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def _draw_item(self, name: str, num: int, colour: str) -> None:
"""
Creates and binds (if a callback exists) a single tk.Label in the InventoryView frame.
name is the name of the item, num is the quantity currently in the users inventory,
and colour is the background colour for this item label (determined by the type of item).
"""
def handlerAdaptor(function, **kwds):
return lambda event, fun = function, kwds = kwds: fun(event, **kwds)
def handler(event, item_name):
self.callback(item_name)
label = tk.Label(self, text=f"{name}: {str(num)}", background=colour)
label.bind("<Button-1>", handlerAdaptor(handler, item_name=name))
label.pack(side='top', fill=tk.X)
这里我们在函数内部又定义了两个中间函数用来实现目的,handler增加了event参数来接收bind函数交给回调函数的Event,但此时不能直接绑定,因为绑定时无处获取Event作为实参传入。
所以又定义了handlerAdaptor来实现一个不含event在参数表中的函数,使用lambda表达式来解决这一问题,虽然参数表中不包含event,但在实际调用的过程中,bind函数依然会传入event参数(我猜想bind传入的参数的名字恰好就是event,所以这个匿名函数才能成立),使得lambda表达式的参数表匹配,返回调用handler的结果。
写两个中间函数的好处是(其实也比较牵强):handler函数可以规范一下回调函数的参数表命名,我们可以不去管self.callback中的参数实际名字是如何,直接用item_name这一参数名去传入即可。
但实际上,如果self.callback的参数表是已知的,我们只需要一个中间函数即可实现“使用Event的同时绑定带参函数”,如下:1
2
3
4
5
6
7
8def _draw_item(self, name: str, num: int, colour: str) -> None:
def middle(function, **kwds):
return lambda event, fun = function, kwds = kwds: fun(**kwds)
label = tk.Label(self, text=f"{name}: {str(num)}", background=colour)
# 这里已知回调函数中的参数名为item_name
label.bind("<Button-1>", middle(self.callback, item_name=name))
label.pack(side='top', fill=tk.X)
总的来说,使用lambda表达式就可以解决这个问题~
参考:https://blog.csdn.net/tinym87/article/details/6957438
后记
理解的还是很基础,只是知道个大概,以后用到了再总结补充吧。