注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

天地不仁,以万物为Googol!

天行有常,不以物喜,不以己悲……

 
 
 

日志

 
 

用Python,5分钟实现多重派发  

2008-04-04 18:44:26|  分类: 翻译 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

原文:http://www.artima.com/weblogs/viewpost.jsp?thread=101605
作者:Guido van Rossum
2005年三月29日

某人的一些注释和我的一些注释被加到了tips里,鼠标悬停就能看到。当然,需要知道在哪里悬停。某人,你可以找找看……

摘要: 我曾经认为多重派发太过先进,以至于我永远也用不到这个技术。虽然我依旧这么想,但是实现多重派发却很简捷,你可以自己看看这个技术。若要读懂这篇文章,需要一些程序集的知识(Some assembly required);更高级的功能做为练习留给读者。

什么是多重派发?我给一个在我理解这个概念后,自己得出的定义:一个函数,有多个版本,并用不同的参数类型做区分。(一些人的定义更复杂,允许根据参数值来区分不同的函数版本。我这篇文章不涉及这个定义。)

先讲个简单的例子,假设我们希望为两个整形参数,两个浮点参数,或两个字符串参数定义一个函数。当然,我们可以这么实现:

def foo(a, b):
if isinstance(a, int) and isinstance(b, int):
...两个整形参数的代码...
elif isinstance(a, float) and isinstance(b, float):
...两个浮点参数的代码...
elif isinstance(a, str) and isinstance(b, str):
...两个字符串参数的代码...
else:
raise TypeError("unsupported argument types (%s, %s)" % (type(a), type(b)))

但是用这种模式实现太狗血了。(而且也不是很面向对象。不过,在我看来,不管名字怎么说,多重派发也不是面向对象的。)所以,使用多重派发的实现看起来会是什么样子?使用修饰是一个很好的办法:

@multimethod(int, int)
def foo(a, b):
...两个整形参数的代码...

@multimethod(float, float):
def foo(a, b):
...两个浮点参数的代码...

@multimethod(str, str):
def foo(a, b):
...两个字符串参数的代码...

下面的部分会展示如何定义这个多重派发的修饰。这其实很简单:有一个根据函数名注册的全局索引(在这篇文章里是“foo”),由注册的类型元组做索引,指向根据参数传给修饰的对应函数。像这样:

# 这些在'mm'模块里

registry = {}

class MultiMethod(object):
def __init__(self, name):
self.name = name
self.typemap = {}
def __call__(self, *args):
types = tuple(arg.__class__ for arg in args) # 生成器表达式!
function = self.typemap.get(types)
if function is None:
raise TypeError("no match")
return function(*args)
def register(self, types, function):
if types in self.typemap:
raise TypeError("duplicate registration")
self.typemap[types] = function

我希望没有一次给你太多代码;到目前为止,一切都很简单(就让我在这里随意混用“类”和“类型”这两个词吧):

  • 构造函数__init__()用一个给定的name和空的类型映射表,来创建出一个MultiMethod实例。(这里简要说明一下:参数name仅仅用于调试。)
  • 方法__call__()使MultiMethod实例本身可以像函数一样被调用。它会根据传入的参数,到映射表里查找对应的函数,并调用这个函数;如果没有对应的函数,就会抛出TypeError异常。这里最大的简化就是,没有根据子类关系做不精确的查找;一个真正的多重派发实现需要尝试最佳匹配,需要根据子类派生关系查找索引表。这个实现留给读者做练习(或者,如果有需求的话,我可能下周会写个blog;没有最相似的类型时,有可能会有很多微妙的情况要处理。)
  • 方法register()会把一个新的函数加入到类型映射表;调用时必须传入一组类型和一个函数。

我希望你看到这里,可以很容易的理解@multimethod修饰应该返回一个MultiMethod实例,并且设法调用它的register()方法。像这样:

def multimethod(*types):
def register(function):
name = function.__name__
mm = registry.get(name)
if mm is None:
mm = registry[name] = MultiMethod(name)
mm.register(types, function)
return mm
return register

就这样!看上去很少的代码,但确实实现了多重派发。注意在这个情况下仅仅支持对位的参数调用(positional parameters,Python在调用一个定义为func(a, b)的时候,有两种调用方式:一种是对位的调用func(1, 2),参数会按照定义时的顺序传入;另一种是命名的调用func(b=2, a=1),这时参数的顺序与定义时的顺序无关)如果你也想使用命名调用,将会变得含混不清。参数默认值也会影响多重派发,因此,不要这样写:

@multimethod(int, int)
def foo(a, b=10):
...

而应该:

@multimethod(int, int)
def foo(a, b):
...

@multimethod(int)
def foo(a):
return foo(a, 10) # 这里会调用前一个foo!

还有一点需要改进:在应用多种参数类型时,只写一次实现。如果@multimethod修饰支持嵌套,就会很方便了,像这样:

@multimethod(int, int)
@multimethod(int)
def foo(a, b=10):
...

这个稍微修改一下修饰,就可以实现了(这不是线程安全的,不过我想这不是太大的问题,所有这些都只是在导入时完成的。):

def multimethod(*types):
def register(function):
function = getattr(function, "__lastreg__", function)
name = function.__name__
mm = registry.get(name)
if mm is None:
mm = registry[name] = MultiMethod(name)
mm.register(types, function)
mm.__lastreg__ = function
return mm
return register

注意那个用三个参数调用的getattr(),你可能不是很熟悉:如果x.y存在,getattr(x, "y", z)就会返回x.y,否则返回z。所以上面那行代码等同于:

if hasattr(function, "__lastreg__"):
function = function.__lastreg__

你可以把对mm.__lastreg__的赋值放到register()方法里,不过这只会让调用__lastreg__和使用它的代码相隔更远,所以我还是觉得这样更好。当然,在一种更静态的语言里,我们必须先声明__lastreg__属性;在Python里就没必要了。重要的是,这个属性不能用一个“通常的”属性名,不然就会覆盖同名的函数属性。(恩……基本上函数属性没有“正常的”使用方法;使用它们总是为了达到某些“秘密的”目的,因此几乎不会在__xxx__命名空间里产生名字冲突。也许我们应该用一些特别长的名字,比如multimethod_last_registered,或者把所有的东西都塞到MultiMethod里面,这样就能用类似__lastreg的私有变量名。)

  评论这张
 
阅读(515)| 评论(3)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017