python-message v0.2.x 全接触
赖勇浩(http://laiyonghao.com)
declare(topic, *a, **kw) 用来向“公告栏”发布一个消息,可以把它看作 pub() 函数,所有订阅了这个 topic 的回调函数都会被调用到。如上例,当 declare() 数据库连接就绪的时候,所有关注数据库连接的对象都会收到通知;而数据库连接就绪一段时间后才 sub() 这个主题的函数,也会在 sub() 的时候马上被调用,实现了“订阅过去的消息”。假设数据库连接在一段时间后失效了(如数据库宕机),那么可以把“公告栏”的消息撤消,这就需要用到 retract(topic) 函数。
除了 declare/retract 函数对之外,还有两个辅助函数 get_declarations()/has_declaration(topic) 分别用来获取“公告栏”的所有主题和查询某个主题是否已经上了“公告栏”,方便吧?
因为我更喜欢直接使用 message.sub() 等函数,所以借鉴 java/actionscript3 的 package 起名策略,我觉得很不错,比如在应用中定义消息主题常量 FOO = 'com.googlecode.python-message.FOO',这样多个库同时定义 FOO 常量也不容易冲突。除此之外,还有一招就是使用 uuid,如下:
之前在博客发过两篇文章(http://blog.csdn.net/lanphaday/article/details/6065896,http://blog.csdn.net/lanphaday/article/details/6074095)谈到过 python-message 这个自制的订阅/发布模式的 Python 库,但都没有完全介绍它的全部特性。后来也在珠三角技术沙龙(http://techparty.org)上口头讲过多次,虽然有音视频,但检索性不佳。最近春节放假在家,重新思考了 python-message 的一些设计问题,并对订阅的回调函数的形式做了修改,因此新的版本将与 v0.1.x 版本不兼容,新发布的版本使用 v0.2.x 版本号(有趣的是之前的两篇文章使用的是 v0.0.x 版本的接口,正巧与 v0.2.x 兼容,所以反而不需要更改了)。在今天,我将在这篇文章里介绍所有 python-message v0.2 所支持的特性。
安装
python-message 已经提交到 pypi,所以支持使用 easy_install 或 pip 安装,使用前者的话,执行以下命令即可(可能需要管理员权限):easy_install -U message
初体验
安装完毕后,可以编写一个简单的例子体验一下面向消息的编程:import message def hello(name): print 'hello, %s.'%name message.sub('greet', hello) # subscribe message.pub('greet', 'lai') # publishmessage 模块下的 sub 函数提供订阅某个主题(topic)的接口,它接受两个参数:第一个是主题,主题不仅可以使用字符串,只要是 collections.Hashable 的子类都可以,比如 tuple;第二个参数是一个回调函数,它的形参应该能够接受通过 message.pub() 函数发布出来的实参,一般情况下,回调函数是没有返回值的。pub 函数的第一个参数是主题,其后可以接若干个不定参数和关键字参数,所有的这些参数都会被传入该主题的回调函数。所以以上代码的输出是:
hello, lai.
取消订阅
取消订阅某个主题显然是常规需求,把下面的代码加到上节的代码段后面,再执行,输出是一样的,因为后续发布的主题没有任何回调函数订阅了:message.unsub('greet', hello) # unsubscribe message.pub('greet', 'lai') # publishpython-message 的 unsub() 支持在回调函数中直接取消订阅,这个特性可以方便地实现一次性订阅,示例代码如下:
import message def hello(name): print 'hello, %s.'%name message.unsub('greet', hello) message.sub('greet', hello) message.pub('greet', 'lai') message.pub('greet', 'u cann\'t c me.')
中止消息传递
python-message v0.1.x 与 v0.2.x 最大的变化就在此处,在 v0.1.x 中,为了支持中止消息传递,对消息的回调函数增加了一个条件:第一个参数 context 接收来自 message 内部的一个控制变量,如上文中的 hello(name) 函数必须是 hello(context, name),在 hello 中对 context.discontinued 置真值,从而实现中止消息传递。但在真实应用中,需要使用中止消息传递的情况非常少见,所以 context 参数不仅让人手指劳累,还常常引起 pylint/pyflaks 报警告。所以在 v0.2.x 中,我痛下决心,去掉了 context 参数,那么又该如何中止消息传递呢,请见以下代码:import message def hello(name): print 'hello %s' % name ctx = message.Context() ctx.discontinued = True return ctx def hi(name): print 'u cann\'t c me.' message.sub('greet', hello) message.sub('greet', hi) message.pub('greet', 'lai')如你所见,python-message 利用了回调函数的返回值来中止消息传递,因为同一个消息可能会被多个回调函数处理,所以回调函数的返回值本身就没有意义,正好可以用来中止消息传递。不过上面代码的写法有点复杂了,回调函数 hello() 可以通过如下写法简写:
def hello(name): print 'hello %s' % name return message.Context(discontinued = True)
改变调用次序
python-message 是同步调用回调函数的,也就是说谁先 sub 谁先被调用。大部分情况下这样已经能够满足大分需求,但有时需要后 sub 的函数先被调用,所以 message.sub 函数通过一个默认参数来支持的,只需要简单地在调用 sub 的时候加上 front = True,这个回调函数将被插入到所有之前已经 sub 的回调函数之前:sub('greet', hello, front = True)。警告:不要改变消息
因为 python 一直是传引用的,所以当 pub() 一个消息的时候,所有消息处理回调函数接受的实参都是同一份,如果某一个回调函数改变了实参,将影响到后续的回调函数调用,如此引起的 bug 非常难排查出错的位置,所以在些郑重警告:虽然回调函数可以随时改变实参,但最好不要这样做。订阅过去的消息
python-message 不是一般的订阅/发布模式实现,它是面向消息编程的程序库,所以它能够“订阅过去的消息”。这个需求听起来好像不常见,让我来举个简单的例子:你开发一个程序库 foo,foo 的关键函数 bar() 要求调用其时某些资源已经“真正可用”,资源真正可用的意思是指数据库连接已经连上了数据库之类。因为初始化数据库连接并不是 foo 的职责,所以 foo 需要有一个途径来判断数据库是否已经可用,一般地,可以约定某个全局的标识量来实现,但这种方法比较肮脏。python-message 通过 declare/retract 函数对实现“公告栏”的功能,从而实现支持“订阅过去的消息”。declare(topic, *a, **kw) 用来向“公告栏”发布一个消息,可以把它看作 pub() 函数,所有订阅了这个 topic 的回调函数都会被调用到。如上例,当 declare() 数据库连接就绪的时候,所有关注数据库连接的对象都会收到通知;而数据库连接就绪一段时间后才 sub() 这个主题的函数,也会在 sub() 的时候马上被调用,实现了“订阅过去的消息”。假设数据库连接在一段时间后失效了(如数据库宕机),那么可以把“公告栏”的消息撤消,这就需要用到 retract(topic) 函数。
除了 declare/retract 函数对之外,还有两个辅助函数 get_declarations()/has_declaration(topic) 分别用来获取“公告栏”的所有主题和查询某个主题是否已经上了“公告栏”,方便吧?
退化为观察者模式
订阅/发布模式是观察者模式的超集,它不关注消息是谁发布的,也不关注消息由谁处理。但有时候我们也希望某个自己的 class 的也能够更方便地订阅/发布消息,也就是想退化为观察者模式,python-message 同样提供了支持,见以下代码:from message import observable def greet(people): print 'hello, %s.'%people.name @observable class Foo(object): def __init__(self, name): print 'Foo' self.name = name self.sub('greet', greet) def pub_greet(self): self.pub('greet', self) foo = Foo('lai') foo.pub_greet()python-message 提供了类装饰函数 observable(),对任何 class 只需要通过它装饰一下就拥有了 sub/unsub/pub/declare/retract 等方法,它们的使用方法跟全局函数是类似的,在此不述。
topic 起名技巧
observable 虽然看起来不够 pythonic,我个人也不喜欢,但有一个不可否认的优势就是它基本上简单了 topic 名字冲突的问题:因为不同的 observable class instance 是拥有不同的消息系统的。因为我更喜欢直接使用 message.sub() 等函数,所以借鉴 java/actionscript3 的 package 起名策略,我觉得很不错,比如在应用中定义消息主题常量 FOO = 'com.googlecode.python-message.FOO',这样多个库同时定义 FOO 常量也不容易冲突。除此之外,还有一招就是使用 uuid,如下:
uuid = 'bd61825688d72b345ce07057b2555719' FOO = uuid + 'FOO'