 | 级别: 高级 David Mertz (mertz@gnosis.cx), 开发人员, Gnosis Software, Inc.
2005 年 10 月 20 日 Python Enterprise Application Kit (PEAK)是一个用于快速开发和重用应用程序组件的框架。当 Python 自己已经成为一种高级语言之后,PEAK 还可以提供更高级的抽象。最近在 PEAK 中添加的一个功能是创建通用函数,并根据断言进行分派,而不只是根据类型进行分派。听起来很神秘?下面我们来详细了解一下。
Python 是一种相当高级的语言,但是我们为什么只是局限于这一级别的抽象呢?利用 PEAK(Python Enterprise Application Kit),我们可以实现更高级的抽象。
PEAK 是 TransWarp 的后继者,是一个用 Python 开发的用于软件自动化的实验性工具包,是一个用于快速开发和重用应用程序组件的 Python 框架。PEAK 为您提供一个组件架构、组件基础设施,以及各种用于构建应用程序的通用组件和组件框架。
介绍 PEAK 最简单的方法是作为“Phillip J. Eby 最近研究的最疯狂的思想”。不管语气如何,我对此总是有些调侃。尽管 PEAK 已经吸引了像其他中等规模的 Free Software Python 项目一样多的贡献者,但是实际上 PEAK 的方向是由不断变革的目标和最初创建者的兴趣所驱动的。
PEAK 随着这种不断变化的兴趣而变化的一个必然结果是,它在可预见的未来一段时间内将可能有点是“实验性的”。这就是说,我们不用太过担心这个问题 —— 我所尝试过的每个 PEAK 版本都很稳定,而且都提供了一些特性。另外,您现在可以获得 PEAK 最新快照的一个自动更新的 tarball,其中还提供了一个非常友好的 distutils 安装脚本。
从我最后一次介绍 PEAK 到现在的一年时间中(请参阅 可爱的 Python: Python Enterprise Application Kit),PEAK 中所引入的最有趣的一种思想是通用函数(generic function)。本文将重点介绍这种功能,虽然这不过是整个 PEAK 的冰山一角。而且,这种思想可以与我的多分派模块(multimethods)很自然地结合起来,我很高兴看到 PEAK 可以对分派风格进行扩展。
在开始继续讨论通用函数之前,我们有必要先来看一下 PEAK 的 Wiki 主页(您可以查看这个页面的最新状态,请参阅 参考资料 中的链接)上给出的一个 PEAK 的组件图。
图 1. PEAK 的组件
断言分派
一点简短的提示:术语 “predicate dispatch”比“predicative”使用得更多,尽管后者从文法上来说更加适合。如果我们要在 Web 或者库函数中进行搜索,可以尝试一下这个简短的拼写方法。
基于类型的分派
阅读过我之前编写的有关 Gnosis Utilities 模块 gnosis.magic.multimethods 的文章的读者对于多分派都有一个基本的印象了。现在我们回忆一下,大部分 OOP 编程都是单一的分派;这就是说,只有一个指定的对象用来确定要走哪条代码路径。在一个诸如 foo.doIt(other,args,here) 之类的调用中,点号之前的参数的类 —— 也就是 Foo —— 确定了要运行哪些代码;other 等参数的类型可能会在 Foo.doIt() 的 if 语句中进行测试,但是不会直接影响代码的分派。
从概念上来说,一种更加通用的技术是让函数/方法的所有参数都以相同的度量来确定自己的专有程度。在多分派的系统中,诸如 doIt() 之类的通用函数可以专门用来处理各种专用的类型签名。在 gnosis.magic.multimethods() API 中,这可能会类似于清单 1 所示:
清单 1. multimethods.py 中对类型进行多分派
doIt.add_rule((Foo1, Other2, int), func1)
doIt.add_rule((Foo2, Other1, str), func2)
doIt.add_rule((Foo1, Other1, float), func3)
doIt(foo, other, args) # 'foo' is just one co-equal specializer
|
PEAK 的 dispatch 模块也有一个基于类型的分派器,但是目前尚微不足道,因为它只能处理单一分派 —— dispatch.on() 封装程序除了基于普通 Python 类的分派之外还做不了多少事情。尽管如此,看到 PEAK 的基于类型的分派仍然让我们对完整断言分派的语法兴奋不已。注意这些例子都利用了 Python 2.4 中新的 decorator 语法,用于修改所定义的函数或方法。您可以 在早期版本的 Python 中使用 PEAK 的通用函数,但是其语法并不美观(如清单 2 所示):
清单 2. PEAK 分派包中对类型进行单一分派
import dispatch
@dispatch.on('foo')
def doIt(foo, other, args):
"Base generic function of 'doIt()'"
@doIt.when(int)
def doIt(foo, other, args):
print "foo is an int |", other, args
@doIt.when(str)
def doIt(foo, other, args):
print "foo is a str |", other, args
doIt( 1, 'this','that') # -> foo is an int | this that
doIt('x','this','that') # -> foo is a str | this that
|
对于新的类型来说,的确可以添加 dispatch.on() 通用函数签名,而不用修改以前的代码。例如,我可以向前面的代码中添加 @doIt.when(float) 或 @doIt.when(MyClass);如果需要,以后的调用就可以利用它。但是即使不用 PEAK dispatch 包,也有很多方法可以实现同样的功能。
什么是类型化?
类型化是件非常有趣的事情。大部分程序员在考虑变量或对象类型时都会联想起计算机 CPU 的奇特行为 —— 即使是那些使用诸如 Python 这种高级语言的程序员也是如此。int、float 与 long 之间的区别不是数字本身之间的强制区别,而是芯片设计者如何使用寄存器和操作符所引起的一些简单后果。但是从理论上来说,3 和 17 之间的整数类型,与 2^31 和 2^31-1 之间的整数类型同样重要(后者在 Python 中是 int 类型,至少在 32 位的机器上是如此)。实际上,将 IntBetween3And17 作为 int 的一个 子类型 是很有意义的,至少从表面上是如此,而不管继承树是否与此匹配。
当然,在 Python 中我们可以通过 class 语句来创建自己的类型,并在喜欢的层次中安插自己的类型。不过现在,我们仍然可以强制搜集某个特定的类所持有的值。类 IntBetween3And17 在 Python 中并不太难以实现;如果您试图在一个更加受限的类的对象上添加 100,那么返回更加通用的 IntBetween0And1000 的一个实例也不难。
PEAK 的 dispatch 模块所做的是(Eby 和其他贡献者可能这样想,也可能不这样想)创建丰富的参数类型系统,从而增强在原来的 Python 中内嵌的相关类型。然而,dispatch 并没有为创建其他类(内嵌的,或用户定义的)的各种受限成员值提供一些封装程序,而是提供了一种通用函数的方法来实现自己的精心设计的“duck 类型化”(duck 是在 Python 和 Ruby 中经常使用的一个词,意思是“如果某个动物走路像鸭子,叫声也像鸭子,那么我们就可以把它当作是鸭子”)。我们通常并不关心对象是什么;只要它的味道像鸭子一样好就行了,Python 会很高兴地在品尝鸭子的氛围中把它吃下。
下面我们来看一个简单的 doIt() 例子,它使用了一些参数类型化(见清单 3):
清单 3. 使用 PEAK 分派包中的断言分派
import dispatch
@dispatch.generic()
def doIt(foo, other):
"Base generic function of 'doIt()'"
@doIt.when("isinstance(foo,int) and isinstance(other,str)")
def doIt(foo, other):
print "foo is an unrestricted int |", foo, other
@doIt.when("isinstance(foo,str) and isinstance(other,int)")
def doIt(foo, other):
print "foo is str, other an int |", foo, other
@doIt.when("isinstance(foo,int) and 3<=foo<=17 and isinstance(other,str)")
def doIt(foo, other):
print "foo is between 3 and 17 |", foo, other
@doIt.when("isinstance(foo,int) and 0<=foo<=1000 and isinstance(other,str)")
def doIt(foo, other):
print "foo is between 0 and 1000 |", foo, other
doIt( 1, 'this') # -> foo is between 0 and 1000 | 1 this
doIt('x', 1234) # -> foo is str, other an int | x 1234
doIt(10, 'this') # -> foo is between 3 and 17 | 10 this
doIt(20, 'this') # -> foo is between 0 and 1000 | 20 this
doIt(-7, 'this') # -> foo is an unrestricted int | -7 this
try: doIt(2222, 66)
except dispatch.interfaces.NoApplicableMethods:
print "No Applicable Methods" # -> No Applicable Methods
|
注意,您在 @doIt.when() 条件中可以指定的断言可以与 Python 代码中用来编写逻辑处理的 if 语句完全相同。然而,使用通用函数会更好;通用函数自己就会有些或多或少的特性,因此不会在错误的地方使用 elif。功能最为强大的是,在后续代码中可以添加多个 @doIt.when() 条件,doIt() 以后就会开始将这个新条件作为一个候选者进行计算,从而满足特定调用的需求。
避免模糊性
不幸的是,一旦您开始编写通用的断言来描述自己希望在某个特定的函数体中处理的值时,就很容易创建一些模糊的条件。至少我在编写上面这个例子时就碰到了这种情况。比如清单 4 中的例子,就是我第一次编写的样子:
清单 4. 模糊断言描述 foo
@doIt.when("isinstance(foo,int) and isinstance(other,str)")
def doIt(foo, other):
print "foo is an unrestricted int |", foo, other
@doIt.when("3<=foo<=17 and isinstance(other,str)")
def doIt(foo, other):
print "foo is between 3 and 17 |", foo, other
|
每个条件本身都是非常理想的。但是对于 doIt(10,"flaz") 调用来说,两个都是真。PEAK 并不会猜想您可能想要怎样做,而会抛出一个 dispatch.interfaces.AmbiguousMethod 异常。
公平地说,我们毕竟应该指定惟一的条件,至少是一些可以由特殊性确定的条件。但是在碰到这些模糊性之前,我很长时间都认为定义一些断言通用函数就可以了。PEAK 只会在碰到会产生模糊性的调用时进行抱怨,而在可能定义一些模糊函数时却不会产生抱怨。
另外,即使上面这个例子看起来很容易发现这个问题,但是它却会变得更糟。毕竟,我们只不过声明了两个有关 foo 的重叠条件。但是对于某个变量的条件产生的模糊性是应该限制的。它们可以是有关多个变量的,这种模糊性可能只存在于某些特定的值的组合中。例如:
清单 5. 交叉条件中的模糊性
@doIt.when('foo < 10 and bar < 100')
def doIt(foo, bar):
print "Condition 1 |", foo, bar
@doIt.when('foo < 100 and bar < 10')
def doIt(foo, bar):
print "Condition 2 |", foo, bar
doIt(50,5) # -> Condition 2 | 50 5
doIt(5,50) # -> Condition 1 | 5 50
doIt(5,5) # -> raises dispatch.interfaces.AmbiguousMethod
|
这就变成难以调试的程序逻辑了。
封装分派函数
用来降低可能出现的断言模糊性的一种方法是将应用程序的条件分解为单独的通用函数。PEAK 的 dispatch 就提供了一种类似的功能,可以显式地将 next_method 分派到 gnosis.magic.multimethods 或 CLOS 中。
在本文中我们并不会详细介绍 next_method 方法;相反,让我们来集中介绍更加通用的一种对 primary 通用函数进行封装的技术,它可以在前置或后置条件(在 primary 函数之前或之后调用的方法)中使用。另外,与 primary 条件不同,doIt.before() 和 doIt.after() 条件都可以执行多个满足条件的断言。这就与我们在调用 main 之前需要确认的很多前置条件的概念吻合了。在出现模糊性的情况中,执行顺序是任意的(它使用了定义的顺序)。
下面是 doIt() 例子的一个变种,它看起来更加清晰(见清单 6):
清单 6. 前置条件和后置条件的分派
import dispatch
@dispatch.generic()
def doIt(foo, other):
"Base generic function of 'doIt()'"
@doIt.before("isinstance(foo,int)")
def sayType_int(foo, other):
print "foo is an int |",
@doIt.before("isinstance(foo,float)")
def sayType_float(foo, other):
print "foo is a float |",
@doIt.when("3<=foo<=17")
def doIt(foo, other):
print "foo is between 3 and 17 |",
@doIt.when("0<=foo<=1000")
def doIt(foo, other):
print "foo is between 0 and 1000 |",
@doIt.when(dispatch.strategy.default)
def doNothing(foo, other):
pass
@doIt.after("True")
def sayValues(foo, other):
print foo, other
doIt(-17, 'x') # -> foo is an int | -17 x
doIt(1.1, 'x') # -> foo is a float | foo is between 0 and 1000 | 1.1 x
doIt( 9, 'x') # -> foo is an int | foo is between 3 and 17 | 9 x
|
如果您可以声明前置/后置条件,那么就可以减少很多模糊性的发生。
在最后这个例子中有几件事情需要注意。在上一个例子中,我们总是使用相同的 doIt() 名来定义通用函数 doIt() 的独特性。然而,这并不是这种命名模式的要求。每个特性都可以按照您所喜欢的方式进行命名,在适用时最好使用描述性名称。通用函数本身必须使用 decorator 进行命名来表示自己的独特性,并在最终会被分派的调用中使用。另外,专用函数在这个通用框架之外也可以使用。
例如,如果您喜欢,就可以自己调用 sayValues("blah","bloo");在这种情况中,其效果与调用 doIt("blah","bloo") 相同(但这只是因为这些参数都不满足前置条件和后置条件)。
为什么会出现这种问题?
通用函数的概念(尤其是断言分派函数的思想)最初可能有点难以理解。但是断言分派是对(狭窄)类型本身进行多分派的一个很好的扩展。
PEAK dispatch 包中最大的优点是可以对模块更精确而简明地进行模块化。在定义通用函数和一些独特性之后,您就可以自由添加其他独特性了,所有这些都不用对原来的代码进行任何修改(但愿经过了良好的测试)。对于大型的协作或为了简化那些为相关版本进行调整的应用程序来说,这个包显得尤其有用。
虽然在本文中并没有介绍,但是 Phillip J. Eby 已经考虑了很多工作来优化这个分派框架,因此您不需要担心这些有用的机制会显著地影响使用它们的程序的速度。您只需要自如地清理代码就可以了。
参考资料 学习
获得产品和技术
讨论
关于作者
对本文的评价
|  |