Python实现Event回调机制的方法


Posted in Python onFebruary 13, 2019

0.背景

在游戏的UI中,往往会出现这样的情况:

在某个战斗副本中获得了某个道具A,那么当进入主界面的时候,你会看到你的背包UI上有个小红点(意思是有新道具),点击进入背包后,发现新增了道具A,显示个数为1,并且在下个界面中有个使用的按钮由灰色不可使用变成橙色的可使用状态

Python实现Event回调机制的方法

图1. 事件触发说明图

其中这里是由道具获得这个事件,触发了上述的三个行为。如果使用显示调用行为,会使得代码难扩展,易出错,逻辑混乱等问题,如果使用Event回调机制,就会变得十分方便。

其实Event回调机制就是观察者模式,如下图:

Python实现Event回调机制的方法

图2. 观察者模式

在C#中存在(delegate & event)的语义来实现Event回调机制:具体使用如下:

public delegate void NewToolGotEvent();

public class ToolBag
{
 event NewToolGotEvent newToolGotHandler;

 void Start()
 {
  newToolGotHandler += renderRedPoint;
  newToolGotHandler += renderNewTool;
  newToolGotHandler += renderAvaliableUseBtn;
 }

 void renderRedPoint()
 {
  //TODO
 }

 void renderNewTool()
 {
  //TODO
 }

 void renderAvaliableUseBtn()
 {
  //TODO
 }

 void EventHappened()
 {
  newToolGotHandler(); // usage, fill args if necessary
 }
}

如果在Python,可以在注册事件的回调时,带入一个参数callback,在注册函数实体内,存在一个list将callback添加进去,形如:

def register_callback(self, cb):
 self.callbacks.append(cb)

但是这样是一个最为普遍的做法,既然是Python,这里我们有更Pythonic的做法,而且相比于上述的观察者模式,它的做法更加简洁,使用更加方便,接下来我们来解析一下Python实现Event callback的步骤。

1. UML类图

上述案例中,是针对游戏客户端UI的案例。所以我们呈现出的UML图也是与UI相关。如图3所示,它显示了Python中实现Event回调的机制。

Python实现Event回调机制的方法

图3. UML关系图

如上图所示,此机制主要由三个类及他们的实例(instance)组成:UIBase, UIScene, UIDataEvent。

1 . UIBase: 所有UIScene的基类,其实例有scene_id变量,包含两个必要的方法, __init__ 是初始化方法,init_data_listeners方法是将实例中的某些方法, 例如ui_updata_func中包含的UIDataEvent实例(所有的UIDataEvent实例都是单例)遍历,并把ui_update_func注册在每一个UIDataEvent实例中。

2 . UIScene: 场景类,管理某个场景的UI渲染。在其实例中,存在某些方法,例如ui_update_func需要在某些UIDataEvent实例触发时候,也被同时触发调用。ui_update_func在Python中一个bound method object, 它会拥有一个特殊的属性events,即所有需要触发此方法的UIDataEvent实例集合。这个通过装饰器(decorator)来实现,即图中的:

“ui_update_func” is a Python object which add a amount of UIDataEvent instances by Python decorator named “data_listener”

3 . UIDataEvent: 事件类,该类有个类变量_events, 记录了所有的UIDataEvent实例,每一个UIDataEvent实例都是单例,而且都有一个名字,和一个回调方法集合_callbacks, 里面的每一个方法都是在本事件触发后需要回调的方法。实例还有个__iadd__方法,将需要回调的函数cb注册进去。__call__事件触发是实际触发的函数。

2. 代码

上一步讲述了三个类之间的联系与各自的作用,此步展示代码实现相关功能。

a) UIBase.py

首先列出来的是UIBase的类,除了上述的__init__与init_data_listeners方法,还多了destroy方法

# -*- coding: utf-8 -*-
from UIDataNotifier import UIDataEvent
import inspect

class UIBase(object):

 def __init__(self, in_scene_id):
  self.id = in_scene_id
  self.init_data_listeners()

 def init_data_listeners(self):
  """为所有标有@data_listener的成员函数注册事件监听器"""
  for listener_name, listener in inspect.getmembers(self, lambda f: hasattr(f, 'events')):
   for event in listener.events:
    event += listener

 def destroy(self):
  print '%s.destroy' % self.__class__.__name__
  UIDataEvent.clear()

init_data_listener比较难理解,我们看一下built-in的inspect.getmembers的源码:

def getmembers(object, predicate=None):
 """Return all members of an object as (name, value) pairs sorted by name.
 Optionally, only return members that satisfy a given predicate."""
 results = []
 for key in dir(object):
  try:
   value = getattr(object, key)
  except AttributeError:
   continue
  if not predicate or predicate(value):
   results.append((key, value))
 results.sort()
 return results

其实源码的意思就是,在dir(object)的value中找,找到能够满足predicate(value) == True的value,然后将(key, value)收集,进行排序后返回。

放在代码的意思是:

for listener_name, listener in inspect.getmembers(self, lambda f: hasattr(f, 'events')):
   for event in listener.events:
    event += listener

在dir(scene)中找,找到value中存在名叫events的属性, 返回得到是一个list,每个list的元素是一个二元tuple: (key, value),其中key,即listener_name是dir(scene)的属性名,而value, 即listener就是属性对象,这里其实就是包含事件的函数对象,然后遍历listener中的每一个UIDataEvent实例,并将listener注册到event中(+= ==> __iadd__ )

b) UIScene.py

UIScene的代码如下:

# -*- coding: utf-8 -*-
from UIDataNotifier import *
from UIBase import UIBase

class UIScene(UIBase):

 def __init__(self, in_scene_id):
  super(UIScene, self).__init__(in_scene_id)

 @data_listener(OnItemAdded)
 def ui_render_red_point(self, item):
  print 'ui_render_red_point'

 @data_listener(OnItemAdded)
 def ui_render_new_tool(self, item):
  print 'ui_render_new_tool: ' + item

 @data_listener(OnItemAdded)
 def ui_render_avaliable_use_btn(self, item):
  print 'ui_render_avaliable_use_btn'

bag_ui_scene = UIScene(123)

在UIScene中只是要填写对于OnItemAdded这个事件触发之后,需要回调的函数,上述代码中写了三个函数。注意需要在函数上加上装饰器@data_listener(OnItemAdded),这样此函数就会添加一个特殊的属性events,具体装饰器的代码见UIDataNotifier.py。

最后新建一个bag_ui_scene的scene。

c) UIDataNotifier.py

UIDataNotifier.py代码如下:

# -*- coding: utf-8 -*-
import sys

def data_listener(*events):
 def wrapped_f(f):
  f.events = events
  return f
 return wrapped_f

class UIDataEvent(object):
 _events = []
 def __init__(self, name):
  self._name = name
  self._callbacks = []
  UIDataEvent._events.append(self)

 def __iadd__(self, cb):
  self._callbacks.append(cb)
  return self

 def __call__(self, *args, **kwargs):
  for cb in self._callbacks:
   try:
    cb(*args, **kwargs)
   except:
    ex = sys.exc_info()
    print "UIDataNotifier cb error, function:", cb.__name__, ex

 def __repr__(self):
  return 'UIDataEvent %s' % self._name

 @classmethod
 def clear(cls):
  """清空所有事件上的所有监听器,在销毁一个界面的时候调用"""
  for event in cls._events:
   event._cb = []

OnItemAdded = UIDataEvent('OnItemAdded')

data_listener装饰器其实就是声明一个特殊的events属性,并将所有在UIScene中填写的UIDataEvent实例元组集合赋值给它。

__iadd__是将参数cb添加到实例的变量中_callbacks中,此方法在UIBase的init_data_listeners中使用。

__call__是当UIDataEvent实例自调用时,例如OnItemAdded(item),实际调用的函数,在函数体里,会回调_callbacks中的每个方法,这也就是Event回调机制的核心部分,相当于观察者模式的notify方法

最后新建一个OnItemAdded事件。

c) client.py

创建上述几个类之后,使用Event回调就非常简单了,代码如下:

# -*- coding: utf-8 -*-
from UIScene import UIScene
from UIDataNotifier import *

OnItemAdded('liu_xin_biao') #新道具流星镖获得事件发生了

输出:

ui_render_avaliable_use_btn
ui_render_new_tool: liu_xin_biao
ui_render_red_point

3.使用方法

1. 在本模块内增加一个事件定义,并在注释中写明事件的参数及意义。

如果要监听一个事件,请仔细阅读相关注释。

2. 在ui类最顶端import需要的事件及data_listener。

3. 在需要响应该事件的方法(监听器方法)前增加装饰器@data_listener,参数内列出要监听的所有事件。

如:

@data_listener(OnEventA, OnEventB)
def my_listener_method(arg1):
 ...

注意保持监听器方法的参数个数及意义与事件触发的地方一致。

4. 在逻辑代码中适当的位置对事件进行触发。如OnEventA(arg1, ...)

注意:并不是所有与UI的交互都必须使用事件,事件机制是为了方便多对多的交互。比如背包物品改变事件,有多个UI都会监听背包物品的变化,而有多种逻辑都会导致背包物品变化,这时使用事件就比较方便。

4. 总结

本文主要讲述了如何使用Python实现Event回调机制,上述的示例代码参考我的[github-EventCallBack] (https://github.com/csdz/SnapToSnap/tree/master/EventCallBack)。

以上这篇Python实现Event回调机制的方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
浅谈终端直接执行py文件,不需要python命令
Jan 23 Python
Python3正则匹配re.split,re.finditer及re.findall函数用法详解
Jun 11 Python
详解Python 数据库的Connection、Cursor两大对象
Jun 25 Python
解决Mac下首次安装pycharm无project interpreter的问题
Oct 29 Python
python opencv将表格图片按照表格框线分割和识别
Oct 30 Python
Python基础之函数原理与应用实例详解
Jan 03 Python
opencv python在视屏上截图功能的实现
Mar 05 Python
django 数据库 get_or_create函数返回值是tuple的问题
May 15 Python
python golang中grpc 使用示例代码详解
Jun 03 Python
Python多线程的退出控制实现
Aug 10 Python
python list等分并从等分的子集中随机选取一个数
Nov 16 Python
PyTorch中的torch.cat简单介绍
Mar 17 Python
Python socket实现多对多全双工通信的方法
Feb 13 #Python
对python文件读写的缓冲行为详解
Feb 13 #Python
python单线程文件传输的实例(C/S)
Feb 13 #Python
Python 实现文件打包、上传与校验的方法
Feb 13 #Python
使用python3构建文件传输的方法
Feb 13 #Python
对python 自定义协议的方法详解
Feb 13 #Python
Python 实现两个服务器之间文件的上传方法
Feb 13 #Python
You might like
详解WordPress中提醒安装插件以及隐藏插件的功能实现
2015/12/25 PHP
checkbox全选/取消全选以及checkbox遍历jQuery实现代码
2009/12/02 Javascript
jquery随机展示头像代码
2011/12/21 Javascript
javascript记录文本框内文字个数检测文字个数变化
2014/10/14 Javascript
JavaScript基础教程之alert弹出提示框实例
2014/10/16 Javascript
AngularJS入门教程之双向绑定详解
2016/08/18 Javascript
Bootstrap框架安装使用详解
2017/01/21 Javascript
jQuery+ajax实现局部刷新的两种方法
2017/06/08 jQuery
jQuery中extend函数简单用法示例
2017/10/11 jQuery
利用CDN加速react webpack打包后的文件详解
2018/02/22 Javascript
纯异步nodejs文件夹(目录)复制功能
2019/09/03 NodeJs
vue中使用百度脑图kityminder-core二次开发的实现
2019/09/26 Javascript
javascript实现计算器功能
2020/03/30 Javascript
分析Python的Django框架的运行方式及处理流程
2015/04/08 Python
Zabbix实现微信报警功能
2016/10/09 Python
Python实现正弦信号的时域波形和频谱图示例【基于matplotlib】
2018/05/04 Python
Python使用wget实现下载网络文件功能示例
2018/05/31 Python
python基于Selenium的web自动化框架
2019/07/14 Python
Python Web框架之Django框架Model基础详解
2019/08/16 Python
Python实现word2Vec model过程解析
2019/12/16 Python
python实例化对象的具体方法
2020/06/17 Python
html5 canvas 画图教程案例分析
2012/11/23 HTML / CSS
网络工程系信息安全技术专业大学生求职信
2013/10/22 职场文书
家长会学生家长演讲稿
2013/12/29 职场文书
村官学习十八大感想
2014/01/15 职场文书
运动会开幕式主持词
2014/03/28 职场文书
战友聚会主持词
2014/04/02 职场文书
租房协议书样本
2014/08/20 职场文书
具结保证书
2015/01/17 职场文书
2015年化工厂工作总结
2015/05/04 职场文书
离婚案件答辩状
2015/05/22 职场文书
安全教育日主题班会
2015/08/13 职场文书
领导干部学习十八届五中全会精神心得体会
2016/01/05 职场文书
Python图片检索之以图搜图
2021/05/31 Python
详细了解MVC+proxy
2021/07/09 Java/Android
MySQL中LAG()函数和LEAD()函数的使用
2022/08/14 MySQL