2.7.对象抽象

\(2.7\)对象抽象

一、思想概述

  对象系统(\(object\;system\))可以实现对抽象数据的不同表达的共存。

  为实现这样的对象系统,我们需要找到一个普遍函数(\(generic\;function\)),这个函数可以接受某个值的各种表达,下面将介绍实现这一操作的方法。

二、前置知识

1.字符串转换

  为更高效地表达数据,我们希望在\(python\)的交互式对话中自动显示表达式的字符串表示。

  \(python\)规定,每个对象都会产生两种不同的字符串表示:一种是人类可以阅读(\(human-interpretable\))的文本,一种是\(python\)可理解(\(python-interpretable\))的表达式。字符串的构造函数str返回一个人类可读的字符串。(在可能的情况下)而repr函数返回结果等价的python表达式:

1
2
3
4
>>> 12e12
12000000000000.0
>>> print(repr(12e12))
12000000000000.0

  当传入的表达式没有初始值时,repr会返回如下语句:

1
2
>>> repr(min)
'<built-in function min>'

  str\(constructor\)repr语句经常重合,但前者返回一个人类更易读的文本:

1
2
3
4
5
6
>>> from datetime import date
>>> tues = date(2011, 9, 12)
>>> 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
5
>>> bool(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
7
>>> def make_adder(n):
def adder(k):
return n + k
return adder
>>> add_three = make_adder(3)
>>> add_three(4)
7

  我们可以定义一个Adder类实现类似操作:

1
2
3
4
5
6
7
8
>>> class Adder(object):
def __init__(self, n):
self.n = n
def __call__(self, k):
return self.n + k
>>> add_three_obj = Adder(3)
>>> add_three_obj(4)
7

  这样,我们就让类和对象表现得像函数一样,这进一步模糊了函数与数据的界限。

三、多重表示\(multiple\;representations\)

  在编程中,对于程序中的某个数据对象,可能存在多种表达方式。例如复数(\(complex\;number\)),既可以用“实部+虚部”来表示,又可以用“模长+辐角主值”来表示。这使我们希望设计出能处理多重表示(\(multiple\;representations\))的系统

  下面即以复数系统说明这一功能的实现。

1.对两种表示的初始化

  我们从最高层次的抽象着手搭建系统,然后慢慢将其中的功能具体化。所有的复数都属于\(Number\),而数字可以相加与相乘,于是我们可以定义一个基准类Number与方法__add____mul__,这里的__add____mul__是通过addmul等更具体的方法实现的:

1
2
3
4
5
>>> class Number:
def __add__(self,other):
return self.add(other)
def __mul__(self,other):
return self.mul(other)

  然后考虑在复数中实现addmul方法,对add我们用复数的标准形式来计算;对于mul我们用复数的辐角形式来计算:

1
2
3
4
5
6
>>> class 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\))是一系列共享的属性名称与各自行为的详细说明。对于复数系统,接口需要实现四个属性:realimagmagnitudeangle

  为了保证复数算法(\(arithmic\))的正确性,这些属性必须具有一致性。这就是说,(magnitude,angle)表示的复数与(real,imag)表示的复数必须是同一个复数。

3.\(properties\)

  让多个属性维系一个固定的关系是一个新的问题,我们可以通过只存储某种属性、在需要其他属性时单独计算的方式解决这个问题。

  \(python\)中的@property装饰符允许我们在不调用表达式语法的情况下调用函数,通过零参数的方法计算属性。以复数系统为例,我们存储复数的标准形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> from math import atan2
>>> class ComplexRI(Complex):
def __init__(self, real, imag):
self.real = real
self.imag = imag
@property
def magnitude(self):
return (self.real**2+self.imag**2)**0.5
@property
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
10
>>> ri = ComplexRI(5, 12)
>>> ri.real
5
>>> ri.magnitude
13.0
>>> ri.real = 9
>>> ri.real
9
>>> ri.magnitude
15.0

  同样地,我们也可以存储辐角形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> from math import sin, cos, pi
>>> class ComplexMA(Complex):
def __init__(self, magnitude, angle):
self.magnitude = magnitude
self.angle = angle
@property
def real(self):
return self.magnitude * cos(self.angle)
@property
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
6
>>> ma = ComplexMA(2, pi/2)
>>> ma.imag
2.0
>>> ma.angle = pi
>>> ma.real
-2.0

  通过接口方式对多重表示进行编码有优良的性质:每种表达的类都可以独自定义,它们只需要在共享的属性名称、这些属性的行为条件(\(behaviour\;condition\))上达成一致即可。当另一个编程者想再加入一个表达时,他只需要再创建一个具有相同属性的类即可。

四、通用函数

1.概念&引入

  通用函数(\(generic\;function\))是一种函数方法,它可以被用于不同类型的变量。 先前的Complex.add是通用函数,因为它的other变量既可以接受ComplexRI型,也可以接受ComplexMA型。

  这种灵活性的实现是基于 ComeplexRIComplexMA共享一个接口的事实。除了利用接口和信息传递来实现通用函数,还有两种不同方法:类型分派与类型强制。

  现在,我们想将原来的复数系统拓展到实数域,在先前章节中我们已经实现了实数类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> from 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
7
>>> c = ComplexRI(1, 1)
>>> isinstance(c, ComplexRI)
True
>>> isinstance(c, Complex)
True
>>> isinstance(c, ComplexMA)
False

  可以利用isinstance函数实现对某个数是不是实数的判断:

1
2
3
4
5
6
7
8
9
10
>>> def 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
>>> is_real(ComplexRI(1, 1))
False
>>> is_real(ComplexMA(2, pi))
True

  当然,我们也可以通过别的方式实现类型分派。我们可以给每一种数据类型添加一个属性.type_tag,调用该属性会返回该数据类型的字符串表示。这样,我们就可以直接比较两个参数的type_tag了:

1
2
3
4
5
6
7
8
>>> Rational.type_tag = 'rat'
>>> Complex.type_tag = 'com'
>>> Rational(2, 5).type_tag == Rational(1, 2).type_tag
True
>>> ComplexRI(1, 1).type_tag == ComplexMA(2, pi/2).type_tag
True
>>> Rational(2, 5).type_tag == ComplexRI(1, 1).type_tag
False

  处理完类型分派问题,我们编写接受实数与复数进行计算的函数操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def 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
25
>>> class 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类中,所有跨类型函数都作为addersmultipliers的索引。

  在这种基于字典的实现方式是可拓展的,当我们要定义一个Number的新子类时,只需给它定义一个type_tag,然后把对应的跨类型操作加进Number.addersNumber.multiplier中,同时也可以在子类中定义自己的addersmultipliers

3.类型强制

  在对两种完全不相同的数据类型进行操作时,直接实现跨类型函数是最佳选择。但有时,我们可以利用类型系统中隐性的附加结构得到更优解法。

  通常,不同的数据类型并不是完全独立的,一种数据类型可能是另一种数据类型的子类型,例如实数就可以被看作虚部为\(0\)的复数。这样,我们就可以只用Complex.addComplex.mul来实现实数虚数的运算了。像这样转换类型的做法被称作类型强制(\(type\;coercion\))

  可以通过以下函数实现类型强制:

1
2
>>> def 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
18
>>> class 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函数数量。