侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

Python实现Event回调机制的方法

Python  /  管理员 发布于 7年前   210

0.背景

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

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

图1. 事件触发说明图

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

其实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回调的机制。

图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 UIDataEventimport inspectclass 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 UIBaseclass 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 sysdef data_listener(*events): def wrapped_f(f):  f.events = events  return f return wrapped_fclass 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 UIScenefrom UIDataNotifier import *OnItemAdded('liu_xin_biao') #新道具流星镖获得事件发生了

输出:

ui_render_avaliable_use_btnui_render_new_tool: liu_xin_biaoui_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回调机制的方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。


  • 上一条:
    使用Python3+PyQT5+Pyserial 实现简单的串口工具方法
    下一条:
    Python socket实现多对多全双工通信的方法
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在python语言中Flask框架的学习及简单功能示例(0个评论)
    • 在Python语言中实现GUI全屏倒计时代码示例(0个评论)
    • Python + zipfile库实现zip文件解压自动化脚本示例(0个评论)
    • python爬虫BeautifulSoup快速抓取网站图片(1个评论)
    • vscode 配置 python3开发环境的方法(0个评论)
    • 近期文章
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2018-04
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2022-01
    • 2023-07
    • 2023-10
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客