- Preface 前言
- 第1章 引论
- 第2章 编程惯用法
- 第3章 基础语法
- 建议19:有节制地使用 from…import 语句
- 建议20:优先使用 absolute import 来导入模块
- 建议21:i+=1 不等于 ++i
- 建议22:使用 with 自动关闭资源
- 建议23:使用 else 子句简化循环(异常处理)
- 建议24:遵循异常处理的几点基本原则
- 建议25:避免 finally 中可能发生的陷阱
- 建议26:深入理解 None 正确判断对象是否为空
- 建议27:连接字符串应优先使用 join 而不是 +
- 建议28:格式化字符串时尽量使用 .format 方式而不是 %
- 建议29:区别对待可变对象和不可变对象
- 建议30:[]、() 和 {}:一致的容器初始化形式
- 建议31:记住函数传参既不是传值也不是传引用
- 建议32:警惕默认参数潜在的问题
- 建议33:慎用变长参数
- 建议34:深入理解 str() 和 repr() 的区别
- 建议35:分清 staticmethod 和 classmethod 的适用场景
- 第4章 库
- 建议36:掌握字符串的基本用法
- 建议37:按需选择 sort() 或者 sorted()
- 建议38:使用 copy 模块深拷贝对象
- 建议39:使用 Counter 进行计数统计
- 建议40:深入掌握 ConfigParser
- 建议41:使用 argparse 处理命令行参数
- 建议42:使用 pandas 处理大型 CSV 文件
- 建议43:一般情况使用 ElementTree 解析 XML
- 建议44:理解模块 pickle 优劣
- 建议45:序列化的另一个不错的选择 JSON
- 建议46:使用 traceback 获取栈信息
- 建议47:使用 logging 记录日志信息
- 建议48:使用 threading 模块编写多线程程序
- 建议49:使用 Queue 使多线程编程更安全
- 第5章 设计模式
- 第6章 内部机制
- 建议54:理解 built-in objects
- 建议55:init() 不是构造方法
- 建议56:理解名字查找机制
- 建议57:为什么需要 self 参数
- 建议58:理解 MRO 与多继承
- 建议59:理解描述符机制
- 建议60:区别 getattr() 和 getattribute() 方法
- 建议61:使用更为安全的 property
- 建议62:掌握 metaclass
- 建议63:熟悉 Python 对象协议
- 建议64:利用操作符重载实现中缀语法
- 建议65:熟悉 Python 的迭代器协议
- 建议66:熟悉 Python 的生成器
- 建议67:基于生成器的协程及 greenlet
- 建议68:理解 GIL 的局限性
- 建议69:对象的管理与垃圾回收
- 第7章 使用工具辅助项目开发
- 第8章 性能剖析与优化
建议52:用发布订阅模式实现松耦合
发布订阅模式(publish/subscribe或pub/sub)是一种编程模式,消息的发送者(发布者)不会发送其消息给特定的接收者(订阅者),而是将发布的消息分为不同的类别直接发布,并不关注订阅者是谁。而订阅者可以对一个或多个类别感兴趣,且只接收感兴趣的消息,并且不关注是哪个发布者发布的消息。这种发布者和订阅者的解耦可以允许更好的可扩放性和更为动态的网络拓扑,故受到了大家的喜爱。
发布订阅模式的优点是发布者与订阅者松散的耦合,双方不需要知道对方的存在。由于主题是被关注的,发布者和订阅者可以对系统拓扑毫无所知。无论对方是否存在,发送者和订阅者都可以继续正常操作。要实现这个模式,就需要有一个中间代理人,在实现中一般被称为Broker,它维护着发布者和订阅者的关系:订阅者把感兴趣的主题告诉它,而发布者的信息也通过它路由到各个订阅者处。简单的实现如下:
from collections import defaultdict route_table = defaultdict(list) def sub(self, topic, callback): if callback in route_table[topic] return route_table[topic].append(callback) def pub(self, topic, *a, **kw): for func in route_table[topic]: func(*a, **kw)
这个实现非常简单,直接放在一个叫Broker.py的模块中(这显然是单件),省去了各种参数检测、优先处理的需求等,甚至没有取消订阅的函数,但它的确展现了发布订阅模式实现的最基础的结构,它的应用代码也可以运行。
import Broker def greeting(name): print 'Hello, %s.'%name Broker.sub('greet', greeting) Broker.pub('greet', 'LaiYonghao') # 输出 Hello, LiaYonghao.
相对于这个简化版本,blinker和python-message两个模块的实现要完备得多。blinker已经被用在了多个广受欢迎的项目上,比如flask和django;而python-message则支持更多丰富的特性。本节以python-message的使用为例,讲解发布订阅模式的应用场景。
安装python-message相当简单,通过pip安装就可以了。
pip install message
然后简单验证一下。
import message def hello(name): print 'hello, %s.'%name message.sub('greet', hello) message.pub('greet', 'lai')
运行输出如下:
hello, lai.
接下来用它解决一些实际问题。假定你给项目组开发了一个程序库foo,里面有一个非常重要的函数——bar。
def bar(): print 'Haha, Calling bar().' do_sth()
这个函数如此重要,所以你给它加上了一行输出代码,用以输出日志。后来你的这个程序库foo被大量使用了,一直运行得很好,直到又一个新项目拖你过去“救火”,因为出了bug无法查出原因,怀疑是foo的问题。你查看了很久日志,都没有发现他们调用bar()的痕迹,一问,原来他们是用logging的,标准输出在做Daemon的时候被重定向到了/dev/null。在临时修改了输出重定向以后,找到了bug所在,并解决了。然后你开始着手解决这个问题。一开始你想在你的foo库中引入logging,但原来的项目又不用logging,你在程序库里引入logging,但谁来初始化它呢?就算你引入了logging,则你们的项目可能是用logging.getLogger('prjA')获取logger,另一个项目可能是用logging.getLogger('prjB'),日后还有新项目呢!一想到要兼容这么多项目你就头大了。忍痛割爱,把print语句给删除掉吧,又怕日后出了问题自己都找不到bug,那还不是自己加班自己苦。这个时候,不妨让python-message来帮你,轻松改一下bar()函数。
import message LOG_MSG = ('log', 'foo') def bar(): message.pub(LOG_MSG, 'Haha, Calling bar().') do_sth()
在已有的项目中,只需要在项目开始处加上这样的代码,继续把日志放到标准输出。
import message import foo def handle_foo_log_msg(txt): print txt message.sub(foo.LOG_MSG, handle_foo_log_msg)
而在那个使用logging的新项目中,则这样修改:
def handle_foo_log_msg(txt): import logging logging.debug(txt)
甚至在一些不关注底层库的日志项目中,直接无视就可以了。通过message,可以轻松获得库与应用之间的解耦,因为库关注的是要有日志,而不关注日志输出到哪里;应用关注的是日志要统一放置,但不关注谁往日志文件中输出内容,这正与发布订阅模式的应用场景不谋而合。
除了简单的sub()/pub()之外,python-message还支持取消订阅(unsub())和中止消息传递。
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利用回调函数的返回值来实现取消消息传递,非常巧妙(读者可以思考一下为什么能够利用回调函数的返回值)。在上面这个例子中,运行后是看不到“u can't c me.”这一行输出的,因为消息在调用hello()后就中止传递了(Broker使用list对象存储回调函数就是为了保证次序)。
python-message是同步调用回调函数的,也就是说谁先sub谁就先被调用。大部分情况下这样已经能够满足大分需求,但有时需要后sub的函数先被调用,这时message.sub函数通过一个默认参数来支持的,只需要简单地在调用sub的时候加上front=True,这个回调函数将被插到所有之前已经sub的回调函数之前:sub('greet',hello,front=True)。
订阅/发布模式是观察者模式的超集,它不关注消息是谁发布的,也不关注消息由谁处理。但有时候我们也希望某个自己的类的也能够更方便地订阅/发布消息,也就是想退化为观察者模式,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等方法,它们的使用方法跟全局函数是类似的,在此不赘述。
注意
因为python-message的消息订阅默认是全局性的,所以有可能产生名字冲突。在减少名字冲突方面,可以借鉴java/actionscript3的package起名策略,比如在应用中定义消息主题常量FOO='com.googlecode.python-message.FOO',这样多个库同时定义FOO常量也不容易冲突。除此之外,还有一招就是使用uuid,如下:
uuid = 'bd61825688d72b345ce07057b2555719' FOO = uuid + 'FOO'
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论