Python中使用函数构建对象

一、使用对象构建抽象

1.1 数据抽象

现在到了数学抽象中最关键的一步:让我们忘记这些符号所表示对象。……根本不必考虑它们到底代表着什么东西。

上一篇文章主要强调的是对数据的操作以及这些操作之间的组合与抽象,这个过程中使用到的主要工具是函数。对于数据自身,我们同样可以应用组合与抽象的基本技巧。数据抽象类似于函数抽象。当我们创建函数抽象时,函数如何实现的细节被隐藏了,而且特定的操作序列本身可以被任何具有相同行为的函数替换。换句话说,我们可以构造抽象来使函数的使用方式和函数的实现细节分离。与之相似,数据抽象是一种方法论,使我们将复合数据对象的使用细节与它的构造方式隔离。

数据抽象的基本概念是构造操作抽象数据的程序。也就是说,我们的程序应该以一种确定的、公认的方式来使用数据,并对数据做出尽可能少的假设。另一方面,需要定义具体的数据表示,独立于使用数据的程序。这两部分的接口是一系列函数,叫做选择器(constructor)和构造器(getter)(注意函数式的思想中没有“修改器”或“setter”等覆写数据的接口),它们基于具体表示实现了抽象数据。

通常,数据抽象的底层概念是,基于某个值的类型的操作如何表达,并为这个值的类型确定一组基本的操作,之后使用这些操作来操作数据。因此可以将抽象数据类型当做一些选择器和构造器的集合,并带有一些行为条件。只要满足了行为条件,这些函数就组成了数据类型的有效表示。因此整理一下这部分的思路:我们认为数据符合某种类型,唯一的要求是这个数据符合这个类型的接口合约,因此我们是以数据对外的表现行为来作为数据是否是某种特征类型的判断标准,这实际上是所谓的“鸭子类型”。这种思想是一种指导性的哲学,这算是一种形式主义吗?

前一篇文章我们提到了,嵌套定义的函数会封装其函数“直接外层作用域”中的局部环境,这使得函数自身具有了携带数据的能力,因此,假设我们想实现一个偏序对,常规的思想是使用一个数组(元组),但是如果我们使用闭包,并对外提供一致的选择器与构造器的接口,那么就可以认为,我们通过一组行为抽象出了某种特定的数据类型。而闭包实际上的类型是函数呀?的确,就像SICP中说的,数据和函数的界限在这里被模糊了,因为此时认为闭包是“带有数据的函数”,也可以认为它是“带有操作的数据”。

偏序对自身具有数据类型的封闭性,这种封闭性是指,如果一种组合数据允许自身使用相同的方式进行组合,那么这种组合数据就满足封闭性。封闭性在任何组合手段中都是核心能力,因为它允许我们创建层次数据结构。偏序对如果满足第一个元素一定是基本数据类型,第二个数据类型要么是偏序对要么为空的话,这样构造的偏序对实际上是一种序列类型,并且是递归形式的序列类型,因为:一个非空序列被看做第一个元素加上序列的剩余部分构成。对于偏序对,已经有了用于操作它的“动作函数”,接下来就可以通过已有的对偏序对的较低层的抽象,来构造序列的接口,比如:构造,按下标读元素以及获取长度以及十分重要的映射。从这个角度看待序列,序列是iterable的,对iterable接口的统一,是Python中for-range循环的基础。python中有很多对象均实现了序列基本抽象(len与下标访问),比如提到的tuple对象与range对象,关于可迭代对象的更多内容,可以参考我的python cookbook笔记(四)上述的基本数据抽象使用起来不能满足我们的要求,因此python扩展了序列抽象,额外的需要满足的(行为)接口是:inindexcount、切片。

在复合数据的处理中,我们强调了数据抽象如何让我们设计程序而不陷入数据表示的细节,以及抽象如何为我们保留灵活性来尝试备用表示。而对于更复杂的处理,我们可以规定一种映射,这种映射接受一个序列并返回一个序列,而映射内部可以使用enumerate->filter->map的通用流程来构造。python中还提供了生成器语法<map expression> for <name> in <sequence expression> if <filter expression>对这一流程进行构造。另一方面,还提供一种对序列的操作方式,即reduce操作。MapReduce正是对大数据处理的一种框架,spark的RDD编程模型就是对这种计算框架的一种表示,Transformation将RDD转换成新的RDD,Action将RDD转换为具体结果。生成器表达式是使用iterable接口约定的特化语法。这些表达式包含了 map 和 filter 的大部分功能,但是避免了被调用函数的实际创建(顺便也避免了环境帧的创建)。

1.2 例子:约束传播系统

先看一个例子,这个代码实现了一个简单的约束传播系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 代码片1.2.1
from operator import mul, truediv, add, sub

def constant(connector, value):
"""The constraint that connector = value."""
constraint = {}
connector['set_val'](constraint, value)
return constraint

def multiplier(a, b, c):
"""The constraint that a * b = c."""
return make_ternary_constraint(a, b, c, mul, truediv, truediv)

def adder(a, b, c):
"""The constraint that a + b = c."""
return make_ternary_constraint(a, b, c, add, sub, sub)

def make_ternary_constraint(a, b, c, ab, ca, cb):
"""The constraint that ab(a,b)=c and ca(c,a)=b and cb(c,b) = a."""
def new_value():
av, bv, cv = [connector['has_val']() for connector in (a, b, c)]
if av and bv:
c['set_val'](constraint, ab(a['val'], b['val']))
elif av and cv:
b['set_val'](constraint, ca(c['val'], a['val']))
elif bv and cv:
a['set_val'](constraint, cb(c['val'], b['val']))
def forget_value():
for connector in (a, b, c):
connector['forget'](constraint)
constraint = {'new_val': new_value, 'forget': forget_value}
for connector in (a, b, c):
connector['connect'](constraint)
return constraint

def inform_all_except(source, message, constraints):
"""Inform all constraints of the message, except source."""
for c in constraints:
if c != source:
c[message]()

def make_connector(name=None):
"""A connector between constraints."""
# 为关注连接器设置一个名字以在变化时获得输出信息
informant = None # 当前信息来自于谁(一个connnector只能有一个)
constraints = []
def set_value(source, value):
nonlocal informant
val = connector['val']
if val is None:
informant, connector['val'] = source, value
if name is not None:
print(name, '=', value)
inform_all_except(source, 'new_val', constraints)
else:
if val != value:
print('Contradiction detected:', val, 'vs', value)
def forget_value(source):
nonlocal informant
if informant == source:
informant, connector['val'] = None, None
if name is not None:
print(name, 'is forgotten')
inform_all_except(source, 'forget', constraints)
connector = {'val': None,
'set_val': set_value,
'forget': forget_value,
'has_val': lambda: connector['val'] is not None,
'connect': lambda source: constraints.append(source)}
return connector

上面的代码以闭包的方式构建对象,具体方式是make_xxx函数会返回字典,字典的键通常对应一个函数,函数会操作局部环境内的值。具体来说connector(连接器)是一种对象,它“持有”一个值,并且可能会参与一个或多个约束。可以认为是“焊锡”。约束(constraint)也是一种对象,负责根据若干个输入的值计算其他输入或者判断有无冲突。可以把约束看做是一个带有引脚的芯片,芯片根据内部电路设计,当给某个引脚电压激励的时候,该芯片会在符合规则的情况下将其他引脚予以激活。这里只实现了三元约束。

当连接器被提供一个值时(被用户或被连接到它的约束器),它会唤醒所有其他相关的约束,通知它们自己得到了一个值。每个唤醒的约束之后会调查它的连接器,来看看是否有足够的信息来为该约束上的未激活连接器求出一个值。如果可以,约束器会设置这个连接器,连接器之后会唤醒所有相关的约束,以此类推。另外,连接器要记住是哪个约束对自己施加了激励,比如一个焊锡连接了三个引脚,那么该焊点的电压只能由一个引脚的输出控制,另外两个引脚只能是被动接受,如果两个引脚对一个焊点施加不同的电压激励,那么这个认为是发生了冲突(在该实现中,即使两个引脚对一个焊点施加相同的激励也认为是出现冲突)。而且只有“激励施加引脚”有能力使当前焊点电压归零(即forgot)。

1.3 局部状态

局部状态使得数据现在出现了一点小问题。当一切都是不可变数据的时候,会导致一种“引用透明”的结果,即在任何一处,将名字直接替换为名字上绑定的值都没有问题。而且判断两个变量是否相等也是十分简单,假设a=4并且b=4那么他们就是一个东西,可以放心的让他们指向同一个实体。然而当抽象数据绑定局部状态后,判断两个对象是否相同就出现了分歧。假设p1=Person("Tom")p2=Person("Tom"),那么他们是一个对象吗?还是说他们只是恰好拥有相同局部状态的不同对象。出于对可变对象的考虑,python中对不可变对象(更准确的说,是intern对象)的比较,is==的效果完全一致,而可变对象则需要考虑是比较状态还是比较身份。

多次调用闭包,每次的调用返回的内部函数会封装一个独立的局部环境,这个特点使得闭包可以用做对象的替代品(其实就是可以这么用,并且在python中这样甚至会更快一点)。上面的约束传播系统中,利用字典返回了一个含有具有多个“行为”并且带有局部状态的数据抽象。

另外需要注意的是,闭包对于局部变量的封装性很好,只有在内部函数定义内才可以访问局部变量。比如下面的代码中,目前我还没有找到一个好的方法改变obj中的val。但是方法不出意外肯定是有的……以后知道了这个方法我会回来更新的。(TODO)

1
2
3
4
5
6
7
8
9
10
# 代码片1.2.2
def makeclosure():
val = 1
def myprint():
print(val)
dic = {'print':myprint}
return dic

obj = makeclosure()
obj['print']

而且闭包目前来看对集成等面向对象的概念支持的不是特别自然。

1.4 面向对象编程OOP

对象创建了数据使用和实现之间的抽象界限。就像可变的数据结构,对象拥有局部状态,并且不能直接从全局环境访问。每个对象将局部状态和行为绑定,以一种方式在数据抽象背后隐藏二者的复杂性。对象自身的行为叫做“方法”(method),是函数(function)的特例。在python中可以使用type观察到这两者的区别,比如,在代码片2.3.1中,可以输入type(A.get_val)type(obj.get_val)查看区别。

类允许继承,继承顺序的问题没有正确的解法,因为我们可能会给某个派生类高于其他类的优先级。但是,任何支持多重继承的编程语言必须始终选择同一个顺序,便于语言的用户预测程序的行为。

类、方法、继承和点运算符的特化语法都可以让我们在程序中形成对象隐喻,它能够提升我们组织大型程序的能力。特别是,我们希望我们的对象系统在不同层面上促进关注分离。抽象界限强制了大型程序不同层面之间的边界。面向对象编程适合于对系统建模,这些系统拥有相互分离并交互的部分。在表现这种系统的时候,程序中的对象通常自然地映射为被建模系统中的对象,类用于表现它们的类型和关系。另一方面,类可能不会提供用于实现特定的抽象的最佳机制。函数式抽象提供了更加自然的隐喻,用于表现输入和输出的关系。一个人不应该强迫自己把程序中的每个细微的逻辑都塞到类里面,尤其是当定义独立函数来操作数据变得十分自然的时候。函数也强制了关注分离。类似 Python 的多范式语言允许程序员为合适的问题匹配合适的范式。为了简化程序,或使程序模块化,确定何时引入新的类,而不是新的函数,是软件工程中的重要设计技巧,这需要仔细关注。

1
2
3
4
5
6
7
8
9
10
# 代码片1.3.1
class A:
def __init__(self):
self.val = 1
def get_val(self):
return self.val

obj=A()
print(type(A.get_val))
print(type(obj.get_val))

可以发现闭包和对象在某种程度上具有相似之处。数据抽象需要属性(局部数据)与方法(行为),方法定义在类中,而实例属性通常在构造器中赋值,二者都是面向对象编程的基本元素。这两个概念很大程度上类似于上面的闭包实现的makeXXX返回的分发字典。注意数据抽象中所反映的“消息传递”隐喻,闭包使用字符串键接受消息,而对象使用点运算符接受消息并执行行为。对象也拥有具名的局部状态值(实例属性),并使用点运算符访问和操作。消息传递的核心概念,就是数据应该通过响应消息而拥有行为,这些消息和它们所表示的抽象类型相关。还有一点不同的是,对象的属性可以直接使用名称更改,比如代码片2.3.1中的obj就可以直接使用obj.val对局部属性进行访问和更改,而代码片2.2.2中的obj则不行。

类和对象本身可以使用函数和字典来表示。以这种方式实现对象系统的目的是展示使用对象隐喻并不需要特殊的编程语言。即使编程语言没有面向对象系统,程序照样可以面向对象。为了实现对象,我们需要抛弃点运算符(它需要语言的内建支持),并创建分发字典,它的行为和内建对象系统的元素差不多。我们已经看到如何通过分发字典实现消息传递行为。为了完整实现对象系统,我们需要在实例、类和基类之间发送消息,它们全部都是含有属性的字典。从这个角度上看,对象和类的关系与子类和基类的关系,本质是相似的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# 代码片1.3.2
def bind_method(value, instance):
"""Return a bound method if value is callable, or value otherwise."""
if callable(value):
def method(*args):
return value(instance, *args)
return method
else:
return value

def make_instance(cls):
"""Return a new object instance, which is a dispatch dictionary."""
def get_value(name):
if name in attributes:
return attributes[name]
else:
value = cls['get'](name)
return bind_method(value, instance)
def set_value(name, value):
attributes[name] = value
attributes = {}
instance = {'get': get_value, 'set': set_value}
return instance

def make_class(attributes, base_class=None):
"""Return a new class, which is a dispatch dictionary."""
def get_value(name):
if name in attributes:
return attributes[name]
elif base_class is not None:
return base_class['get'](name)
def set_value(name, value):
attributes[name] = value
def new(*args):
return init_instance(cls, *args)
cls = {'get': get_value, 'set': set_value, 'new': new}
return cls

def init_instance(cls, *args):
"""Return a new object with type cls, initialized with args."""
instance = make_instance(cls)
init = cls['get']("__init__") # 规定了消息名
if init:
init(instance, *args)
return instance

def make_account_class():
"""Return the Account class, which has deposit and withdraw methods."""
def __init__(self, account_holder):
self['set']('holder', account_holder)
self['set']('balance', 0)
def deposit(self, amount):
"""Increase the account balance by amount and return the new balance."""
new_balance = self['get']('balance') + amount
self['set']('balance', new_balance)
return self['get']('balance')
def withdraw(self, amount):
"""Decrease the account balance by amount and return the new balance."""
balance = self['get']('balance')
if amount > balance:
return 'Insufficient funds'
self['set']('balance', balance - amount)
return self['get']('balance')
return make_class({'__init__': __init__,
'deposit': deposit,
'withdraw': withdraw,
'interest': 0.02})

def make_checking_account_class(baseclass):
"""Return the CheckingAccount class, which imposes a $1 withdrawal fee."""
def withdraw(self, amount):
return baseclass['get']('withdraw')(self, amount + 1)
return make_class({'withdraw': withdraw, 'interest': 0.01}, baseclass)


Account = make_account_class()
jim_acct = Account['new']('Jim')
jim_acct['get']('deposit')(20)
jim_acct['get']('withdraw')(5)

CheckingAccount = make_checking_account_class()
jack_acct = CheckingAccount['new']('Jack')
jack_acct['get']('deposit')(20)
jack_acct['get']('withdraw')(5)

我们的构建在字典上的对象系统十分类似于 Python 内建对象系统的实现。Python 中,任何用户定义类的实例,都有个特殊的__dict__属性,将对象的局部实例属性储存在字典中,就像我们的attributes字典那样。Python 的区别在于,它区分特定的特殊方法,这些方法和内建函数交互来确保那些函数能正常处理许多不同类型的参数。操作不同类型参数的函数会在下面的内容中介绍。实际上,这种方式构建的对象也好,类也好,都是字典,功能是消息传递,即根据接收到的“消息”执行行为、操纵局部数据,因此,后面统一称其为“消息传递字典”或者“分发字典”。

代码片2.3.2中,make_XXX_class调用make_class函数来定义一个类,因此这个函数的作用等价于正常的具有对象系统的语言中的class XXX的作用。在make_XXX_class函数内需要做的是定义一个字典,字典负责将字符串映射为方法或者数据(类属性),然后将这个字典传递给make_class。这个字典实现的其实就是所谓的“消息传递”。

make_class接受两个参数,第一个是“消息传递”字典,第二个是基类。这个函数会创建一个闭包,闭包自身携带的局部状态就是传入的用于消息传递的类属性字典与基类列表。make_class需要做的共性的工作是为闭包带有的局部信息提供行为,主要是三种:“get”,“set”,以及“new”。getset负责查找与修改类属性字典中的值(注意,可能是数据也可能是方法),查找的过程首先在自己的属性字典中查找,如果查找不到则递归到基类中查找,这里可以发现是以递归形式定义的基类,即make_class只允许传入一个基类,但是基类自己也可能有基类,这个思路十分类似于使用偏序对构建列表。但是这也使得不支持多继承的特性。set则只能修改本类的属性字典,这个十分好理解,子类是没有权限修改基类的,但是可以对基类的属性(或方法)进行覆盖。new对应的行为方法就是调用init_instance去构造对象实例了。最后将绑定了行为方法与类属性的字典返回,这个返回其实就是类的数据抽象,注意局部状态其实也绑定进去了,不过不是直接的绑定在了字典上,而是通过局部环境栈绑定在了方法中。

init_instance对应一个类接受“new”消息时执行的构造对象的行为。接受的第一个参数是class自身的绑定字典,后面接收不定量参数,用于传给特定对象的__init__。主要干两个事,一个是使用make_instance创建一个对象对应的分发字典,并负责get(要负责生成bind-method)和set的实现。这一步骤对应python自身的__new__方法,之后再尝试使用cls的分发字典中绑定的__init__方法为当前对象进行初始化赋值。__init__方法接受对象实例自身和具体的不定长的初始化参数,这也是为什么在类的定义的各种方法中,都要让第一个参数是self,就是用于绑定实例自身的。在make_instance会看到其他对象实例上的其他方法也会进行这种绑定。

make_instance是构造类的对象的方法,此时构造的对象是尚未初始化的对象(还不具有自己的对象属性)。基本原理和make_class一样,是建立一个“消息传递”字典并返回。其作用也是在内部建立消息传递字典并绑定上setget方法。有几点细节的不同。第一,make_class的闭包绑定的局部数据是属性字典(不是消息传递字典)与基类的消息传递字典,而make_instance返回的闭包所绑定的局部数据有对象自身的属性字典与类的消息传递字典。前者在自身的类属性字典中找不到时,递归的到基类中执行get行为,后者在自身的对象属性字典中找不到时到类中寻找。第二,这一点是较为实质上的区别,就是类中的get行为就是简单的返回自身或者基类的属性字典中的内容,不管值是数据还是函数(此时就是普通函数,回想type(A.f)type(obj.f)的区别),而对象的get行为会分析是否是在自身的属性字典中发现的还是类的属性字典中发现的,如果是在类的属性字典中发现的并且返回的是可调用对象,那么会通过bind_method将对象实例自身绑定到函数的第一个参数上。这使得在对象执行方法的时候可以使不同的对象共享的类函数有能力访问对象自身的局部数据。另外注意对象的set行为只会设置对象自己的属性字典,这使得对象不能动态的绑定方法,只能动态的绑定数据,这一点和python内置的对象系统不同。

bind_method需要解释的不多,其工作是检查从类中get到的是数据还是函数,如果是数据直接返回,如果是函数,将对象实例设置为函数的第一个参数。

上面的代码没有实现完整的 Python 对象系统,它包含这篇文章没有涉及到的特性(比如元类和静态方法),不带有多重继承和内省行为(比如返回实例的类)。这里的实现并不遵循 Python 类型系统的明确规定,但是我们可以通过上述代码了解到实现对象隐喻中使用到的核心思想。

1.4 泛用方法

前面的内容中引入了复合数据类型,以及由构造器和选择器实现的数据抽象机制。使用消息传递,我们就能使抽象数据类型直接拥有行为。使用对象隐喻,我们可以将数据的表示和用于操作数据的方法绑定在一起,从而使数据驱动的程序模块化,并带有局部状态。在通常的消息系统中,点运算符用于对象之间的消息传递,我们在这一节探索更多的对象之间的组合与操作的方式。

首先,对于一个对象,我们可能希望生成一个字符串,当作为 Python 表达式解释时,求值为等价的对象。Python规定,所有对象都应该能够产生两种不同的字符串表示:一种是人类可解释的文本(通过str),另一种是Python可解释的表达式(通过repr)。对于后者,要求生成的字符串是合理的python表达式,并且可以通过eval求值为等价的对象。对于内建的基本类型,都满足这一点,但是怎样使得用户自定义的类也满足这种要求呢?消息传递提供了这个问题的解决方案:众所周知,repr函数在参数上调用叫做__repr__的函数,通过在用户定义的类上实现同一方法,我们就可以将repr的适用性扩展到任何我们以后创建的类。这个例子强调了消息传递的另一个普遍的好处:就是它提供了一种机制,用于将现有函数的职责范围扩展到新的对象。这实际上是python中对于多态的一种处理方法,思考多态的概念:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。特定函数应该作用于多种数据类型。这里举例的消息传递方法仅仅是多态函数实现家族的一员。可以自己思考一下,C++是怎么实现多态的?(虚函数)

其次,对于一个数据抽象,其表示方法可能有多种并且有各自的适用场景,比如负数可以有直角坐标表示和极坐标表示,前者更适用于复数的加法,后者则适用于乘法。这种时候可以为每种表示均设计一个类的实现并统一二者的选择器和构造器的接口。编码多种表示的接口拥有良好的特性。用于每个表示的类可以独立开发;它们只需要遵循它们所共享的属性名称。这个接口同时是递增的。如果另一个程序员希望向相同程序添加第三个复数表示,它们只需要使用相同属性创建另一个类。

对于每种表示对应的实现,需要重载其乘法和加法行为。以加法为例,在python中,Python 会检查表达式的左操作数和右操作数上的特殊方法。首先,Python会检查左操作数的__add__方法,之后检查右操作数的__radd__方法。如果二者之一被发现,这个方法会以另一个操作数的值作为参数调用。

最后,我们需要设计更加泛用的函数。此时,我们可以使用operator.mul计算任意两种表示的复数的乘积,但是如果将要计算一个复数与一个简单的整数的乘法或加法是无法做到的。那么怎么改良?一种思路是使用字典根据参数类型进行分发,由于在python中,函数也是对象,所以我们可以直接把这个字典绑定到函数上使其成为函数对象的一个属性。这个完全泛用的实现方法的方式叫做数据导向编程。这种解决方法的问题是要为每一种可能的类型组合与编写计算函数并绑定到字典上,假设类型可能是复数或者整数,计算方式那么分发字典就要有四项。如果类型有三种,分发字典就要有九项。

另一种是思路是使用强制类型转换,虽然我们仍然需要编程强制转换函数来关联类型,我们仅仅需要为每对类型编写一个函数,而不是为每个类型组合和每个泛用方法编写不同的函数。我们所期望的是,类型间的合理转换仅仅依赖于类型本身,而不是要调用的特定操作。而且,通过迭代的强制类型转换可以减少我们编写的强制类型转换的代码。比如,复数类型可以不考虑编写接受整数的强制类型转换,而是通过代理的方式先将整数转化为有理数,然后再转化为复数。强制类型转换的问题是,一些类型是无法转换的,比如一个复数不能转化为一个整数,反之却可以。另外,强制类型转换会导致精度的损失,比如有理数可以已完全精确的方式存储,如果将其转换为浮点数,数据精度就会有损失。

Python 的早期版本拥有对象上的__coerce__特殊方法。最后,内建强制转换系统的复杂性并不能支持它的使用,所以被移除了。反之,特定的操作按需强制转换它们的参数。运算符被实现为用户定义类上的特殊方法,比如__add____mul__。这完全取决于你,取决于用户来决定是否使用类型分发,数据导向编程,消息传递,或者强制转换来在你的程序中实现泛用函数。在考虑C++中的泛型算法是基于什么设计的?(迭代器模式?)

1.4.1 functools.singledispatch

在使用上,singledispatch实际上是一个装饰器,目标是简化类型分发字典的部分,程序员只需要着眼完成不同类型对应的实现方式就可以。使用起来也十分的简单。下面给出python3.5的文档中给出的示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from functools import singledispatch

@singledispatch
def fun(arg, verbose=False):
if verbose:
print("Let me just say,", end=" ")
print(arg)

@fun.register(int)
def _(arg, verbose=False):
if verbose:
print("Strength in numbers, eh?", end=" ")
print(arg)

@fun.register(list)
def _(arg, verbose=False):
if verbose:
print("Enumerate this:")
for i, elem in enumerate(arg):
print(i, elem)

具体实现可以看Python源码。在源码中,关于dispatch_cachecache_token的功能我没有看明白,感觉是为了支持用户自定义的抽象基类的。如果去掉和这两个变量相关的部分,可以发现这部分内容很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def singledispatch(func):
"""Single-dispatch generic function decorator.
Transforms a function into a generic function, which can have different
behaviours depending upon the type of its first argument. The decorated
function acts as the default implementation, and additional
implementations can be registered using the register() attribute of the
generic function.
"""
registry = {}


def dispatch(cls):
"""generic_func.dispatch(cls) -> <function implementation>
Runs the dispatch algorithm to return the best available implementation
for the given *cls* registered on *generic_func*.
"""
try:
impl = registry[cls]
except KeyError:
impl = _find_impl(cls, registry)
dispatch_cache[cls] = impl
return impl

def register(cls, func=None):
"""generic_func.register(cls, func) -> func
Registers a new implementation for the given *cls* on a *generic_func*.
"""
# 感觉这个转发技巧挺好玩的
if func is None:
return lambda f: register(cls, f)
registry[cls] = func
return func

def wrapper(*args, **kw):
return dispatch(args[0].__class__)(*args, **kw)

registry[object] = func
wrapper.register = register
wrapper.dispatch = dispatch
wrapper.registry = MappingProxyType(registry)
update_wrapper(wrapper, func)

return wrapper

对于上述代码大部分功能实时易于理解的,说几个关键点。

  1. registy就是真正的类型分发字典,键是类型,值是对应的实现函数。
  2. dispatch函数中的_find_impl是当当前类型没有是,尝试使用mro算法序列化其基类,然后检查基类有没有对应的方法。
  3. register中的一个简单的自己调用自己的小技巧大大简化了函数的设计。
  4. wrapper是返回的闭包对象,使用函数对象绑定方法,实际上和2.3节中我们自己实现对象系统中,返回一个分发字典本质是一样的,不过这里返回一个函数对象就可以使用点操作符了。
  5. update_wrapper功能和我们平常使用@wraps目的是一样的,因为后者实际上就是堆前者的封装。update_wrapper作用是使用第二个参数的函数名和文档替换第一个函数的。
  6. MappingProxyTypetypes模块中的方法。它的功能是对原map给出一个只读的动态的视图,我们不可以通过返回的代理修改原map,但是如果原映射做出了改动,我们可以通过这个视图观察到。在这里的目的是对于内部的registry封装为私有变量,通过wrapper.registry是不可修改类型分发字典的,只有通过wrapper.register函数可以写原字典。

参考

  1. 《SICP-py》第二章
本站总访问量