2.7.对象抽象
\(2.7\)对象抽象
一、思想概述
对象系统(\(object\;system\))可以实现对抽象数据的不同表达的共存。
为实现这样的对象系统,我们需要找到一个普遍函数(\(generic\;function\)),这个函数可以接受某个值的各种表达,下面将介绍实现这一操作的方法。
二、前置知识
1.字符串转换
为更高效地表达数据,我们希望在\(python\)的交互式对话中自动显示表达式的字符串表示。
\(python\)规定,每个对象都会产生两种不同的字符串表示:一种是人类可以阅读(\(human-interpretable\))的文本,一种是\(python\)可理解(\(python-interpretable\))的表达式。字符串的构造函数str
返回一个人类可读的字符串。(在可能的情况下)而repr
函数返回结果等价的python
表达式:
1
2
3
412e12
12000000000000.0
print(repr(12e12))
12000000000000.0
当传入的表达式没有初始值时,repr
会返回如下语句:
1
2repr(min)
'<built-in function min>'
str
的\(constructor\)与repr
语句经常重合,但前者返回一个人类更易读的文本:
1
2
3
4
5
6from datetime import date
2011, 9, 12) tues = date(
repr(tues)
'datetime.date(2011, 9, 12)'
str(tues)
'2011-09-12'
对象系统为我们提供了能用于所有数据类型的方法:__repr__
与__str__
,这让我们可以以点表达式编写程序:
1
2
3
4
5 tues.__repr__()
'datetime.date(2011, 9, 12)'
tues.__str__()
'2011-09-12'
2.特殊方法
在\(python\)中,特殊方法(\(special\;method\))在特定情况被\(python\)编译器调用,例如:
__init__
方法会在某个对象被创建后自动调用。__str__
方法在打印结果时自动调用。__repr
方法在交互式程序展示值(value
)时自动调用。
\(python\)中还有一些类似的特殊方法。
\(a.\)__bool__
方法
一般的bool
会默认\(0\)为false
,其他值为true
。对于对象而言,\(python\)中默认所有对象都有true
值。但我们可以利用__bool__
方法来覆盖这一默认值。例如我们将值为\(0\)的\(Account\)为false
:
1
>>>Account.__bool__=lambda self:self.balance!= 0
则对于下面的对象,它会输出false
: 1
2
3
4
5bool(Account('Jack'))
False
if not Account('Jack'):
print('Jack has nothing')
Jack has nothing
\(b.\)序列操作
序列中的方法主要如下:
__len__
1
2'Go Bears!'.__len__()
9__getitem__
1
2
3
4'Go Bears!'[3]
'B'
'Go Bears!'.__getitem__(3)
'B'
3.可调用的对象
通过__call__
方法,我们可以让定义的对象像函数一样被调用,进而实现类似高阶函数的功能。
考虑下面的高阶函数: 1
2
3
4
5
6
7def make_adder(n):
def adder(k):
return n + k
return adder
3) add_three = make_adder(
4) add_three(
7
我们可以定义一个Adder
类实现类似操作:
1
2
3
4
5
6
7
8class Adder(object):
def __init__(self, n):
self.n = n
def __call__(self, k):
return self.n + k
3) add_three_obj = Adder(
4) add_three_obj(
7
这样,我们就让类和对象表现得像函数一样,这进一步模糊了函数与数据的界限。
三、多重表示\(multiple\;representations\)
在编程中,对于程序中的某个数据对象,可能存在多种表达方式。例如复数(\(complex\;number\)),既可以用“实部+虚部”来表示,又可以用“模长+辐角主值”来表示。这使我们希望设计出能处理多重表示(\(multiple\;representations\))的系统。
下面即以复数系统说明这一功能的实现。
1.对两种表示的初始化
我们从最高层次的抽象着手搭建系统,然后慢慢将其中的功能具体化。所有的复数都属于\(Number\),而数字可以相加与相乘,于是我们可以定义一个基准类Number
与方法__add__
、__mul__
,这里的__add__
、__mul__
是通过add
、mul
等更具体的方法实现的:
1
2
3
4
5class Number:
def __add__(self,other):
return self.add(other)
def __mul__(self,other):
return self.mul(other)
然后考虑在复数中实现add
与mul
方法,对add
我们用复数的标准形式来计算;对于mul
我们用复数的辐角形式来计算:
1
2
3
4
5
6class Complex(Number):
def add(self,other):
return ComplexRI(self.real+other.real,self.imag+other.imag)
def mul(self,other):
magnitude=self.magnitude*other.magnitude
return ComplexMA(magnitude,self.angle+other.angle)
这种实现方式假设了复数的两种类表示。
2.接口
对象的属性作为信息传递(\(message\;passing\))的一种,允许不同的数据类型对相同信息做出不同反应。这种在不同的类中引发相似操作的共享信息组(\(shared\;set\;of\;messages\))
是有力的抽象方式。而接口(\(interface\))是一系列共享的属性名称与各自行为的详细说明。对于复数系统,接口需要实现四个属性:real
,imag
,magnitude
,angle
。
为了保证复数算法(\(arithmic\))的正确性,这些属性必须具有一致性。这就是说,(magnitude,angle)
表示的复数与(real,imag)
表示的复数必须是同一个复数。
3.\(properties\)
让多个属性维系一个固定的关系是一个新的问题,我们可以通过只存储某种属性、在需要其他属性时单独计算的方式解决这个问题。
\(python\)中的@property
装饰符允许我们在不调用表达式语法的情况下调用函数,通过零参数的方法计算属性。以复数系统为例,我们存储复数的标准形式:
1
2
3
4
5
6
7
8
9
10
11
12
13from math import atan2
class ComplexRI(Complex):
def __init__(self, real, imag):
self.real = real
self.imag = imag
def magnitude(self):
return (self.real**2+self.imag**2)**0.5
def angle(self):
return atan2(self.imag, self.real)
def __repr__(self):
return 'ComplexRI({0:g}, {1:g})'.format(self.real, self.imag)
这样我们就可以通过点表达式直接访问除标准形式外的属性了:
1
2
3
4
5
6
7
8
9
105, 12) ri = ComplexRI(
ri.real
5
ri.magnitude
13.0
9 ri.real =
ri.real
9
ri.magnitude
15.0
同样地,我们也可以存储辐角形式: 1
2
3
4
5
6
7
8
9
10
11
12
13from math import sin, cos, pi
class ComplexMA(Complex):
def __init__(self, magnitude, angle):
self.magnitude = magnitude
self.angle = angle
def real(self):
return self.magnitude * cos(self.angle)
def imag(self):
return self.magnitude * sin(self.angle)
def __repr__(self):
return 'ComplexMA({0:g}, {1:g} * pi)'.format(self.magnitude, self.angle/pi)
并得到以下的输出结果: 1
2
3
4
5
62, pi/2) ma = ComplexMA(
ma.imag
2.0
ma.angle = pi
ma.real
-2.0
通过接口方式对多重表示进行编码有优良的性质:每种表达的类都可以独自定义,它们只需要在共享的属性名称、这些属性的行为条件(\(behaviour\;condition\))上达成一致即可。当另一个编程者想再加入一个表达时,他只需要再创建一个具有相同属性的类即可。
四、通用函数
1.概念&引入
通用函数(\(generic\;function\))是一种函数方法,它可以被用于不同类型的变量。
先前的Complex.add
是通用函数,因为它的other
变量既可以接受ComplexRI
型,也可以接受ComplexMA
型。
这种灵活性的实现是基于
ComeplexRI
与ComplexMA
共享一个接口的事实。除了利用接口和信息传递来实现通用函数,还有两种不同方法:类型分派与类型强制。
现在,我们想将原来的复数系统拓展到实数域,在先前章节中我们已经实现了实数类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16from fractions import gcd
class Rational(Number):
def __init__(self,numer,denom):
g = gcd(numer,denom)
self.numer=numer // g
self.denom=denom // g
def __repr__(self):
return 'Rational({0}, {1})'.format(self.numer, self.denom)
def add(self,other):
nx,dx=self.numer, self.denom
ny,dy=other.numer, other.denom
return Rational(nx*dy+ny*dx,dx*dy)
def mul(self,other):
numer=self.numer*other.numer
denom=self.denom*other.denom
return Rational(numer,denom)
我们希望实现一个通用函数__add__
实现所有数的加法,同时将实数与复数的概念分离。
2.类型分派
一个实现跨类(\(cross-type\))操作的方法是基于参数的数据类型选择适当的行为。而类型分派(\(type\;dispatch\))正是为了实现一个检查参数数据类型的函数。
\(python\)内置的isinstance
函数可以实现这一功能。它接收一个对象和一个类,返回这个对象属于该类或属于该类的继承类的真假:
1
2
3
4
5
6
71, 1) c = ComplexRI(
isinstance(c, ComplexRI)
True
isinstance(c, Complex)
True
isinstance(c, ComplexMA)
False
可以利用isinstance
函数实现对某个数是不是实数的判断:
1
2
3
4
5
6
7
8
9
10def is_real(c):
"""Return whether c is a real number with no imaginary part."""
if isinstance(c, ComplexRI):
return c.imag == 0
elif isinstance(c, ComplexMA):
return c.angle % pi == 0
1, 1)) is_real(ComplexRI(
False
2, pi)) is_real(ComplexMA(
True
当然,我们也可以通过别的方式实现类型分派。我们可以给每一种数据类型添加一个属性.type_tag
,调用该属性会返回该数据类型的字符串表示。这样,我们就可以直接比较两个参数的type_tag
了:
1
2
3
4
5
6
7
8'rat' Rational.type_tag =
'com' Complex.type_tag =
2, 5).type_tag == Rational(1, 2).type_tag Rational(
True
1, 1).type_tag == ComplexMA(2, pi/2).type_tag ComplexRI(
True
2, 5).type_tag == ComplexRI(1, 1).type_tag Rational(
False
处理完类型分派问题,我们编写接受实数与复数进行计算的函数操作:
1
2
3
4
5
6
7
8
9
10
11
12
13def add_complex_and_rational(c, r):
return ComplexRI(c.real + r.numer/r.denom, c.imag)
def mul_complex_and_rational(c, r):
r_magnitude, r_angle = r.numer/r.denom, 0
if r_magnitude < 0:
r_magnitude, r_angle = -r_magnitude, pi
return ComplexMA(c.magnitude * r_magnitude, c.angle + r_angle)
def add_rational_and_complex(r, c):
return add_complex_and_rational(c, r)
def mul_rational_and_complex(r, c):
return mul_complex_and_rational(c, r)
利用类型分派,我们可以根据传入参数的类型选择适当的函数操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Number:
def __add__(self, other):
if self.type_tag == other.type_tag:
return self.add(other)
elif (self.type_tag, other.type_tag) in self.adders:
return self.cross_apply(other, self.adders)
def __mul__(self, other):
if self.type_tag == other.type_tag:
return self.mul(other)
elif (self.type_tag, other.type_tag) in self.multipliers:
return self.cross_apply(other, self.multipliers)
# to tackle cross-type requirement
def cross_apply(self, other, cross_fns):
cross_fn = cross_fns[(self.type_tag, other.type_tag)]
return cross_fn(self, other)
# dispatch dictionary
adders = {("com", "rat"): add_complex_and_rational,
("rat", "com"): add_rational_and_complex}
multipliers = {("com", "rat"): mul_complex_and_rational,
("rat", "com"): mul_rational_and_complex}
>>>ComplexRI(1.5,0)+Rational(3,2)
ComplexRI(3,0)
>>>Rational(-1,2)*ComplexMA(4,pi/2)
ComplexMA(2,1.5*pi)
在新定义的Number
类中,所有跨类型函数都作为adders
与multipliers
的索引。
在这种基于字典的实现方式是可拓展的,当我们要定义一个Number
的新子类时,只需给它定义一个type_tag
,然后把对应的跨类型操作加进Number.adders
和Number.multiplier
中,同时也可以在子类中定义自己的adders
和multipliers
。
3.类型强制
在对两种完全不相同的数据类型进行操作时,直接实现跨类型函数是最佳选择。但有时,我们可以利用类型系统中隐性的附加结构得到更优解法。
通常,不同的数据类型并不是完全独立的,一种数据类型可能是另一种数据类型的子类型,例如实数就可以被看作虚部为\(0\)的复数。这样,我们就可以只用Complex.add
与Complex.mul
来实现实数虚数的运算了。像这样转换类型的做法被称作类型强制(\(type\;coercion\))。
可以通过以下函数实现类型强制: 1
2def rational_to_complex(r):
return ComplexRI(r.numer/r.denom,0)
包含类型强制的Number
类实现如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Number:
def __add__(self, other):
x,y=self.coerce(other)
return x.add(y)
def __mul__(self, other):
x,y=self.coerce(other)
return x.mul(y)
def coerce(self, other):
if self.type_tag==other.type_tag:
return self,other
elif (self.type_tag, other.type_tag) in self.coercions:
return (self.coerce_to(other.type_tag), other)
elif (other.type_tag, self.type_tag) in self.coercions:
return (self, other.coerce_to(self.type_tag))
def coerce_to(self, other_tag):
coercion_fn=self.coercions[(self.type_tag, other_tag)]
return coercion_fn(self)
coercions={('rat', 'com'): rational_to_complex}
类型强制的方法相对于类型分派有新的优势:虽然需要一个coerce_to
函数实现类型转换,但我们只需要写这一个函数,就可以将其他所有的函数归进一个统一的范式中。这是基于以下思想:类型间的转换只与类型本身有关、与类型的具体操作无关。
类型强制给我们带来了启发:对于两种数据类型,我们可以设法找到一种中间类型(即前文说的“隐性的附加结构”),将两种数据类型转换成这个类型。例如我们可以将矩形和菱形都转化为平行四边形。类似的,不同的中间类型可以转换为新的中间类型\(\cdots\)通过这样的链式强制(\(chaining\;coercion\)),我们可以减少程序中需要的coerce
函数数量。