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 相关文章推荐
python使用xlrd模块读写Excel文件的方法
May 06 Python
django admin添加数据自动记录user到表中的实现方法
Jan 05 Python
python实现对任意大小图片均匀切割的示例
Dec 05 Python
python模糊图片过滤的方法
Dec 14 Python
用Python编写一个高效的端口扫描器的方法
Dec 20 Python
利用python提取wav文件的mfcc方法
Jan 09 Python
Python多线程threading模块用法实例分析
May 22 Python
Python基础学习之类与实例基本用法与注意事项详解
Jun 17 Python
使用python打印十行杨辉三角过程详解
Jul 10 Python
Python OpenCV调用摄像头检测人脸并截图
Aug 20 Python
Tensorflow tf.nn.depthwise_conv2d如何实现深度卷积的
Apr 20 Python
Django在Model保存前记录日志实例
May 14 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
php 强制下载文件实现代码
2013/10/28 PHP
TNC vs IO BO3 第一场2.13
2021/03/10 DOTA
获取任意Html元素与body之间的偏移距离 offsetTop、offsetLeft (For:IE5+ FF1 )[
2006/12/22 Javascript
jquery插件珍藏(图片局部放大/信息提示框)
2013/01/08 Javascript
原生javaScript实现图片延时加载的方法
2014/12/22 Javascript
JavaScript采用递归算法计算阶乘实例
2015/08/04 Javascript
一个仿微博登陆邮箱提示框js开发案例
2016/07/28 Javascript
jQuery实现鼠标经过像翻页和描点链接效果
2016/08/08 Javascript
jQuery实现搜索页面关键字的功能
2017/02/16 Javascript
原生js仿淘宝网商品放大镜效果
2017/02/28 Javascript
JavaScript闭包_动力节点Java学院整理
2017/06/27 Javascript
基于JavaScript实现无缝滚动效果
2017/07/21 Javascript
一个Vue视频媒体多段裁剪组件的实现示例
2018/08/09 Javascript
[04:01]2014DOTA2国际邀请赛 TITAN告别Ohaiyo期望明年再战
2014/07/15 DOTA
[43:36]Liquid vs Mineski 2018国际邀请赛小组赛BO2 第一场 8.18
2018/08/19 DOTA
Pandas标记删除重复记录的方法
2018/04/08 Python
Python的多维空数组赋值方法
2018/04/13 Python
对python实现二维函数高次拟合的示例详解
2018/12/29 Python
Python自定义一个类实现字典dict功能的方法
2019/01/19 Python
django-rest-framework解析请求参数过程详解
2019/07/18 Python
Django QuerySet查询集原理及代码实例
2020/06/13 Python
python中strip(),lstrip(),rstrip()函数的使用讲解
2020/11/17 Python
Mytheresa英国官网:拥有160多个奢侈品品牌
2016/10/09 全球购物
英国领先的运动物理治疗供应公司:Vivomed
2018/07/14 全球购物
澳大利亚最早和最古老的巨型游戏专家:Yardgames
2020/02/20 全球购物
Quiksilver美国官网:始于1969年的优质冲浪服和滑雪板外套
2020/04/20 全球购物
新西兰最大的连锁超市:Countdown
2020/06/04 全球购物
Linux如何命名文件--使用文件名时应注意
2014/05/29 面试题
求职自荐信范文格式
2013/11/29 职场文书
优秀社区干部事迹材料
2014/02/03 职场文书
财务主管岗位职责
2014/02/28 职场文书
空乘英文求职信
2014/04/13 职场文书
横幅标语大全
2014/06/17 职场文书
群众路线问题查摆对照检查材料
2014/10/04 职场文书
董事长秘书岗位职责
2015/02/13 职场文书
《传颂之物 虚伪的假面》BD发售宣传CM公开
2022/04/04 日漫