如果您正尝试去处理元类,或者正受困于 Twisted 中的异步编程,或者正在研究由于使用了多分派而使您精疲力尽的面向对象编程,那么您完全错了!PEAK 将所有这些中的一些要素组合到了一个组件编程框架中。PEAK 还存在一些小问题。类似于 Twisted,PEAK 的文档 -- 尽量数量巨大 -- 难以看懂。但是尽管如此,关于 Python 领袖 Phillip J. Eby 领导的这一项目还是有一些东西非常值得关注;而且,我觉得,有机会进行极具生产价值的并且层次特别高的应用程序开发。
PEAK 包由许多不同用途的子包组成。一些重要的子包是 peak.api、 peak.binding、 peak.config、 peak.naming 和 peak.storage 。那些名字大部分是自我解释性的。子包 peak.binding 用于组件间的灵活连接; peak.config 让您可以存储“很少改变的(lazily immutable)”数据,这些数据与声明性应用程序(declarative application )编程有关; peak.naming 让您可以为(网络的)资源创建全局惟一的标识符; peak.storage 顾名思义让您可以管理数据库和持久内容。
不过,对本文来说,我们将关注的是 peak.api 。特别是 PyProtocols 包,它可以单独获得并为其他 PEAK 子包提供一个基础设施。在 peak.api.protocols 中包括了 PyProtocols 包的一个版本。不过现在我所感兴趣的是研究一个独立的 protocols 包。在以后的部分,我将返回来讨论 PEAK 其他部分的话题。
什么是协议?
抽象地说,协议只是对象同意遵循的一组行为。强类型(Strongly-typed)编程语言 -- 包括 Python -- 都有一个基本类型的集合,每个基本类型都有一组得到保证的行为:Integer 知道如何去求它们自己的乘积;list 知道如何去遍历它们的内容;dictionary 知道如何根据一个关键字找到相应的值;file 知道如何去读和写字节;诸如此类。您可以预期的内置类型的行为集合构成了它们实现的一个 协议。对协议进行系统化的对象被称为 接口(interface)。
对标准的类型而言,将实现的所有行为全部列出并不太困难(尽管不同的 Python 版本之间会稍有不同;或者,不同的编程语言之间当然会有差别)。但是,在边界 -- 对于属于自定义类的对象来说 -- 难以声明最终是什么构成了“类-dictionary”或“类-file”的行为。大部分情况下,只实现了比如内置的 dict 类型的方法的一个子集 -- 甚至是相当小的子集 -- 的自定义对象,就足够“类-dictionary”而可以满足当前的要求。不过,能显式地整理出一个对象要用到的函数、模块、类或者框架中需要能够做哪些事情,将是很吸引人的。那就是 PyProtocols 包所做到的(一部分)。
在具有静态类型声明的编程语言中,为了在新的上下文中使用数据,您通常需要将其自一个类型 强制类型转换(cast)或者 转换(convert)到另一个类型。在其他语言中,转换根据上下文的需要隐式地进行,这些被称为 强迫同型(coercions)。Python 中既有强制类型转换也有强迫同型,通常使用更多的是前者(“显式优于隐式”)。您可以将向一个浮点数加到一个整型数,结果得到一个更为通用的浮点数;但是如果您希望将字符串 "3.14" 转换为一个数字,那么您需要使用显式的构造函数 float("3.14") 。
PyProtocols 具有一个称为“适配(adaptation)”的功能,类似于“部分类型(partial typing)”这一非正统计算机科学概念。适配还可能被认为是“加速的强制同型”。如果一个接口定义了所需要的一组能力 (也就是对象方法),那么要去做“所需要的一切”的对象就要求适配 -- 通过 protocols.adapt() 函数实现 -- 以提供所需要的能力。显然,如果您有一个显式的转换函数可以将类型 X 的对象转换为类型 Y 的对象(在这里 Y 实现了某个 IY 接口),那么那个函数要能够让 X 适配协议 IY 。不过,PyProtocols 中的适配可以做比这多得多的事情。例如,甚至如果您从来没有显式地编写过从类型 X 到类型 Y 的转换程序, adapt() 通常可以推演出一条让 X 提供 IY 所要求的能力的途径(也就是说,找到从 X 到接口 IZ ,从 IZ 到 IW ,然后再从 IW 到 IY 的中间转换)。
声明接口和适配器
在 PyProtocols 中有很多不同的方法可以创建接口和适配器。PyProtocols 文档非常详细地介绍了这些技术 -- 很多不会在本文中涉及。接下来我们将进入一些细节,不过,我觉得,在这里给出实际的 PyProtocols 代码的一个最简化实例是个有用的方法。
例如,我决定创建一个 Python 对象的类-Lisp 序列化。其描述并不是准确的 Lisp 语法,我也并不在意这种格式确切的优点和缺点。在这里,我的想法只是创建一个功能,使之可以执行类似 repr() 函数或 pprint 模块的工作,不过结果是既与以前串行器(serializers)有明显的不同,又要能更容易地扩展/定制。出于举例说明的目的做出了一个非常不像 Lisp 的选择:映射(mappings)是一个比列表(list)更为基础的数据结构(Python 的元组(tuple)或列表被作为以连续整数为键的映射来处理)。下面是代码:
lispy.py PyProtocol 定义
from protocols import *
from cStringIO import StringIO
# Like unicode, & even support objects that don't explicitly support ILisp
ILisp = protocolForType(unicode, ['__repr__'], implicit=True)
# Class for interface, but no methods specifically required
class ISeq(Interface): pass
# Class for interface, extremely simple mapping interface
class IMap(Interface):
def items():
"A requirement for a map is to have an .items() method"
# Define function to create an Lisp like representation of a mapping
def map2Lisp(map_, prot):
out = StringIO()
for k,v in map_.items():
out.write("(%s %s) " % (adapt(k,prot), adapt(v,prot)))
return "(MAP %s)" % out.getvalue()
# Use this func to convert an IMap-supporting obj to ILisp-supporting obj
declareAdapter(map2Lisp, provides=[ILisp], forProtocols=[IMap])
# Note that a dict implements an IMap interface with no conversion needed
declareAdapter(NO_ADAPTER_NEEDED, provides=[IMap], forTypes=[dict])
# Define and use func to adapt an InstanceType obj to the ILisp interface
from types import InstanceType
def inst2Lisp(o, p):
return "(CLASS '(%s) %s)" % (o.__class__.__name__, adapt(o.__dict__,p))
declareAdapter(inst2Lisp, provides=[ILisp], forTypes=[InstanceType])
# Define a class to adapt an ISeq-supporting obj to an IMap-supporting obj
class SeqAsMap(object):
advise(instancesProvide=[IMap],
asAdapterForProtocols=[ISeq] )
def __init__(self, seq, prot):
self.seq = seq
self.prot = prot
def items(self): # Implement the IMap required .items() method
return enumerate(self.seq)
# Note that list, tuple implement an ISeq interface w/o conversion needed
declareAdapter(NO_ADAPTER_NEEDED, provides=[ISeq], forTypes=[list, tuple])
# Define a lambda func to adapt str, unicode to ILisp interface
declareAdapter(lambda s,p: "'(%s)" % s,
provides=[ILisp], forTypes=[str,unicode])
# Define a class to adapt several numeric types to ILisp interface
# Return a string (ILisp-supporting) directly from instance constructor
class NumberAsLisp(object):
advise(instancesProvide=[ILisp],
asAdapterForTypes=[long, float, complex, bool] )
def __new__(klass, val, proto):
return "(%s %s)" % (val.__class__.__name__.upper(), val)
</div>
在上面的代码中,我已经用一些不同的方法声明了许多适配器。在一些情况中,代码将一个接口转换到另一个接口;在其他情况中,类型本身直接适配到另一个接口。我希望您能注意到关于代码的一些方面:(1)没有创建任何从 list 或 tuple 到 ILisp 接口的适配器;(2)没有为 int 数字类型显式声明适配器;(3)就此而言,没有声明直接由 dict 到 ILisp 的适配器。下面是代码将如何适配( adapt() )各种 Python 对象:
test_lispy.py 对象序列化
from lispy import *
from sys import stdout, stderr
toLisp = lambda o: adapt(o, ILisp)
class Foo:
def __init__(self):
self.a, self.b, self.c = 'a','b','c'
tests = [
"foo bar",
{17:2, 33:4, 'biz':'baz'},
["bar", ('f','o','o')],
1.23,
(1L, 2, 3, 4+4j),
Foo(),
True,
]
for test in tests:
stdout.write(toLisp(test)+'\n')
</div>
运行时,我们得到:
test_lispy.py 序列化结果
$ python2.3 test_lispy.py
'(foo bar)
(MAP (17 2) ('(biz) '(baz)) (33 4) )
(MAP (0 '(bar)) (1 (MAP (0 '(f)) (1 '(o)) (2 '(o)) )) )
(FLOAT 1.23)
(MAP (0 (LONG 1)) (1 2) (2 3) (3 (COMPLEX (4+4j))) )
(CLASS '(Foo) (MAP ('(a) '(a)) ('(c) '(c)) ('(b) '(b)) ))
(BOOL True)
</div>
对我们的输出进行一些解释将会有所帮助。第一行比较简单,我们定义了一个直接从字符串到 ILisp 的适配器,对 adapt("foo bar", ILisp) 的调用只是返回了 lambda 函数的结果。下一行只是有一点复杂。没有直接从 dict 到 ILisp 的适配器;但我们不必使用任何适配器就可以让 dict 去适配 IMap (我们声明了足够多),而

