内容


可爱的 Python

多分派

用多元法泛化多态性

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 可爱的 Python

敬请期待该系列的后续内容。

此内容是该系列的一部分:可爱的 Python

敬请期待该系列的后续内容。

什么是多态性?

使用 Python 或其它面向对象编程语言的大多数程序员在使用多态性时都采用了一种较为实用和具体的方式。也许多态性最常见的应用是在创建符合公共协议的一系列对象方面。在 Python 中,这通常只是特别的多态性问题;在其它语言中,较为常见的是声明形式接口和/或这些系列共享一个公共祖先。

例如,有许多函数都是对“类文件”对象进行操作,其中只是通过支持类似 .read().readlines() 也许还有 .seek() 这样的几个方法来定义类文件。类似 read_app_data() 这样的函数可能带有参数 src ,当我们调用该函数时,我们可能决定将该参数传递给本地文件、 urllib 对象、 cStringIO 对象还是一些让该函数调用 src.read() 的定制对象。从每个对象在 read_app_data() 中如何运行这一角度来看,每个对象类型是可交换的。

让我们回头想想这里面真正发生了什么。事实上,我们真正关心的是在上下文中选择要执行的正确代码路径;老式的过程代码可以做出同样的决定,OOP 只不过使代码雅致了些。例如,一个过程(伪)代码片断可能为如下所示:

清单 1. 对象类型上代码路径的过程选择
...bind 'src' in some manner...
if <<src is a file object>>:
    read_from_file(src)
elif <<src is a urllib object>>:
    read_from_url(src)
elif <<src is a stringio object>>:
    read_from_stringio(src)
...etc...

通过安排不同类型的对象来执行公共方法,我们将 分派决定移到对象中并从显式条件块中消除。给定的 src 对象通过查看整个继承树知道它需要调用哪一个代码块。隐式切换仍在继续执行,但是它针对对象 src 的类型进行。

对象 src 对于所有传递给其方法的参数享有特权。OOP 语义使该特权看起来似乎是必然的,其实不然。在许多情况下过程切换只是被置于类的方法主体中。例如,我们可以用伪 Python 实现协议兼容的类 FooBar ,如下所示:

清单 2. Foo 和 bar 实现 .meth() 方法
class Foo:
    def meth(self, arg):
        if <<arg is a Foo>>:
            ...FooFoo code block...
        elif <<arg is a Bar>>:
            ...FooBar code block...
class Bar:
    def meth(self, arg):
        if <<arg is a Foo>>:
            ...BarFoo code block...
        elif <<arg is a Bar>>:
            ...BarBar code block...
# Function to utilize Foo/Bar single-dispatch polymorphism
def x_with_y(x, y):
    if <<x is Foo or Bar>> and <<y is Foo or Bar>>:
        x.meth(y)
    else:
        raise TypeError,"x, y must be either Foo's or Bar's"

调用 x_with_y() 时,可能要执行五个截然不同的代码路径/块。如果 xy 的类型不合适,则给出异常(当然您还可以做别的事)。但是假定类型没问题,那么代码路径 首先由多态分派选中, 其次由过程切换选中。而且, Foo.meth()Bar.meth() 的定义内部的切换在很大程度上是等同的。(单分派类型的)多态性只做了一半的工作。

完善多态性

在单分派多态性中,挑选出“拥有”方法的对象。从语法上说,在 Python 中通过在点之前指定名称来挑选对象 - 跟在点、方法名和左括号后面的只是一个参数。但是从语义上说,对象的特殊之处还在于利用继承树来进行方法解析。

如果不用一种特定方式处理一个对象,而是允许代码块中所包含的每个对象帮助选择正确的代码路径,那会怎样呢?例如,我们可以用一种更对称的方式来表达五路切换:

清单 3. Foo 和 Bar 上的多分派
x_with_y = Dispatch([((object, object), <<exception block>>)])
x_with_y.add_rule((Foo,Foo), <<FooFoo block>>)
x_with_y.add_rule((Foo,Bar), <<FooBar block>>)
x_with_y.add_rule((Bar,Foo), <<BarFoo block>>)
x_with_y.add_rule((Bar,Bar), <<BarBar block>>)
#...call the function x_with_y() using some arguments...
x_with_y(something, otherthing)

我认为对多个参数进行多态分派所具有的这种对称性比先前的样式要雅致得多。还有,对于确定要采取的适当代码路径时所涉及的两个对象,该样式有助于记录它们的同等作用。

标准 Python 不允许配置这种类型的多分派;但幸运的是,您可以使用我所编写的模块 multimethods 来做到这一点。请参阅 参考资料以下载该模块本身或将其作为 Gnosis 实用程序中的一部分。一旦安装了 multimethods 之后,您需要做的是在应用程序的顶部包含这样一行代码:

from multimethods import Dispatch

“多元法(multimethods)”通常是多分派的同义词;但是多元法这个名称暗示着处理较抽象的概念时应采用具体的函数/对象。

Dispatch 实例是个可调用对象,可以用数量不限的规则来配置它。还可以用方法 Dispatch.remove_rule() 来删除规则,这使得使用 multimethods 的多分派比静态类层次结构更加具有动态性(但是您也可以在运行时用 Python 类做一些隐秘的事)。还要注意的是, Dispatch 实例可以接受数目不定的参数;首先对参数的数目进行匹配,其次是其类型。如果用规则中不曾定义的任何模式来调用 Dispatch 实例,则出现 TypeError 。如果您只想在遇到未定义的情况时抛出异常,那么就没必要使用落后的 (object,object) 模式对 x_with_y() 进行初始化。

只要将 Dispatch 的初始化调用中所列出的每个 (pattern,function) 元组传递给 .add_rule() 方法;在初始化时建立规则还是在后来建立规则,程序员觉得哪种方便就采用哪种(可以将它们进行混合再匹配,就像前面的示例那样)。从分派器调用函数时,将调用分派器中使用的参数传递给该函数;您需要确保您使用的函数可以接受与该函数匹配的参数数目。例如,以下这两种调用是等价的:

清单 4. 显式的和分派的函数调用
# Define function, classes, objects
def func(a,b): print "The X is", a, "the Y is", b
class X(object): pass
class Y(object): pass
x, y = X(), Y()
# Explicit call to func with args
func(x,y)
# Dispatched call to func on args
from multimethods import Dispatch
dispatch = Dispatch()
dispatch.add_rule((X,Y), func)
dispatch(x,y)         # resolves to 'func(x,y)'

显然,如果您在设计时就已经知道 xy 的类型,那么建立分派器的机制就是多余的。但是,多态性也具有同样的限制 - 它只在您不能对于每个执行路径将对象约束为单个类型时才有所帮助。

改进继承

多分派不仅仅泛化了多态性,它还在许多上下文中提供了更加灵活的继承方式。这里举了一个示例。假定您正在编写处理各种形状的绘图或 CAD 程序;特别是,您希望能用某一种方式 合并两种形状,这种方式取决于所涉及的这两种形状。而且,要研究的形状集将被派生的应用程序或插件扩展。扩展形状类的集合是一种笨拙的增强技术;例如:

清单 5. 用于功能扩展的继承
# Base classes
class Circle(Shape):
    def combine_with_circle(self, circle): ...
    def combine_with_square(self, square): ...
class Square(Shape):
    def combine_with_circle(self, circle): ...
    def combine_with_square(self, square): ...
# Enhancing base with triangle shape
class Triangle(Shape):
    def combine_with_circle(self, circle): ...
    def combine_with_square(self, square): ...
    def combine_with_triangle(self, triangle): ...
class NewCircle(Circle):
    def combine_with_triangle(self, triangle): ...
class NewSquare(Square):
    def combine_with_triangle(self, triangle): ...
# Can optionally use original class names in new context
Circle, Square = NewCircle, NewSquare
# Use the classes in application
c, t, s = Circle(...), Triangle(...), Square(...)
newshape1 = c.combine_with_triangle(t)
newshape2 = s.combine_with_circle(c)
# discover 'x' of unknown type, then combine with 't'
if isinstance(x, Triangle): new3 = t.combine_with_triangle(x)
elif isinstance(x, Square): new3 = t.combine_with_square(x)
elif isinstance(x, Circle): new3 = t.combine_with_circle(x)

特别是,每个现有的形状类必须在子代类中添加功能,这使维护工作陷入了组合的复杂性和困难当中。

相比之下,多分派技术就简单多了:

清单 6. 用于性能扩展的多元法
# Base rules (stipulate combination is order independent)
class Circle(Shape): pass
class Square(Shape): pass
def circle_with_square(circle, square): ...
def circle_with_circle(circle, circle): ...
def square_with_square(square, square): ...
combine = Dispatch()
combine.add_rule((Circle, Square), circle_with_square)
combine.add_rule((Circle, Circle), circle_with_circle)
combine.add_rule((Square, Square), square_with_square)
combine.add_rule((Square, Circle),
                 lambda s,c: circle_with_square(c,s))
# Enhancing base with triangle shape
class Triangle(Shape): pass
def triangle_with_triangle(triangle, triangle): ...
def triangle_with_circle(triangle, circle): ...
def triangle_with_square(triangle, square): ...
combine.add_rule((Triangle,Triangle), triangle_with_triangle)
combine.add_rule((Triangle,Circle), triangle_with_circle)
combine.add_rule((Triangle,Square), triangle_with_square)
combine.add_rule((Circle,Triangle),
                 lambda c,t: triangle_with_circle(t,c))
combine.add_rule((Square,Triangle),
                 lambda s,t: triangle_with_square(t,s))
# Use the rules in application
c, t, s = Circle(...), Triangle(...), Square(...)
newshape1 = combine(c, t)[0]
newshape2 = combine(s, c)[0]
# discover 'x' of unknown type, then combine with 't'
newshape3 = combine(t, x)[0]

新规则的定义(和对函数/方法的支持)在很大程度上是等同的。但是多分派样式的巨大优势在于您可以用它来无缝地合并未知类型的形状。无需回头使用显式的(和冗长的)条件块,而是用规则定义自动处理问题。更好的做法是,所有合并都用单个可调用 combine() 完成,而不需要使用各式各样的合并方法。

分派传播

无需深入研究分派, multimethods.Dispatch 类将为给定分派器调用选择“最合适”的分派。但是,有时候需要注意:“最好的”并非是“唯一的”。也就是说, dispatch(foo,bar) 调用可能是采用已定义规则 (Foo,Bar) 的较合适的分派,但是也可能与 (FooParent,BarParent) 规则松散(并不是不)配合。就像您有时想对继承方法中的超类方法进行调用一样,您有时还想在分派器内对不太特定的规则进行调用。

multimethods 模块提供了调用不太特定的规则的一条捷径和一种更为精调的方式。在粗糙的层次上,您通常只想在开始或结束执行代码块时自动调用不太特定的规则。同样,您几乎总是在子代方法主体的开始或结束部分调用超类方法。对于对不太特定的方法常见的开始/结束调用,您就可以将那指定为规则的一部分。例如:

清单 7. 自动的分派传播
class General(object): pass
class Between(General): pass
class Specific(Between): pass
dispatch = Dispatch()
dispatch.add_rule((General,), lambda _:"Gen", AT_END)
dispatch.add_rule((Between,), lambda _:"Betw", AT_END)
dispatch.add_rule((Specific,), lambda _:"Specif", AT_END)
dispatch(General())  # Result: ['Gen']
dispatch(Specific()) # Result: ['Specif', 'Betw', 'Gen']

当然,在某些情况中(比如 (General) 规则),在已定义规则中没有什么是不太特定的。但是为了一致性,每次调用分派器都会返回一个从控制传播内容的所有函数中返回的返回值列表。如果在规则中既没有指定 AT_END 也没有指定 AT_START ,那么就不会发生传播(而且返回列表长度为 1)。这说明了 [0] 索引在形状示例中或许看上去很神秘的原因。

精调的传播控制方式就是使用分派器的 .next_method() 方法。为了利用手工传播,您应当使用 .add_dispatchable() 方法而不是 .add_rule() 方法来定义规则。分派函数自身最好还是应当接受 dispatch 参数。调用分派器需要一个分派参数,或者您可以使用 .with_dispatch() 这一便利方法。例如:

清单 8. 用手工传播进行编程
def do_between(x, dispatch):
  print "do some initial stuff"
  val = dispatch.next_method() # return simple value of up-call
  print "do some followup stuff"
  return "My return value"
foo = Foo()
import multimethods
multi = multimethods.Dispatch()
multi.add_dispatchable((Foo,), do_between)
multi.with_dispatch(foo)
# Or: multi(foo, multi)

调用超类方法的许多方式很棘手,而以同样方式对较为不特定的多元法所进行的手工传播也会很棘手。为使事情易于处理, .next_method() 调用始终只返回上行调用的返回值,如果您想象 AT_END 参数那样将这些返回值收集到列表中,那么将需要附加并操作您认为正确的值。但是,在最常见的“用例”中要执行一系列相关的初始化;在这种情况下,返回值往往是不相关的。

关于线程安全的说明

这儿有必要插入一点说明,免得读者遇到问题。由于传播采用有状态方式跟踪调用了哪些(依次不太特定的)规则,所以分派器不是线程安全的。如果您希望在多线程中使用分派器,那么您应该为每个线程“克隆”分派器。这样做不是很耗内存和 CPU 资源,因此克隆分派器并不会付出很大代价。例如,假定可以通过线程调用某个函数;您可以写成:

清单 9. 针对线程安全的克隆
def threadable_dispatch(dispatcher, other, arguments)
    dispatcher = dispatcher.clone()
    #...do setup activities...
    dispatcher(some, rule, pattern)
    #...do other stuff...

如果在 threadable_dispatch() 中没有生成新线程,那就一切正常了。

您花一点时间就可以理解多分派的思想了,如果您精通面向对象编程,那就更不在话下了。但是应用它一段时间后,您可能会发现多分派从一开始就泛化并增强了 OOP 胜过过程编程的优势。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=21371
ArticleTitle=可爱的 Python: 多分派
publish-date=07262003