#|cs61a 课时笔记 对象的抽象

cs61a对象的抽象学习笔记

目录

    • Special Methods
      • True and false value
      • Sequence operations
        • __len__方法
        • __getitem__方法
      • Callable objects
      • Arithmetic
    • Multiple Representations
      • Generic Functions
        • Type dispatching
      • coercion

对象抽象中的一个核心概念是泛型函数,它是一个可以接受多种不同类型的值的函数。我们将考虑实现泛型函数的三种不同技术:共享接口、类型调度和类型强制。Python对象系统支持创建泛型函数的特性。
Special Methods 只要对象被构造,__init__ 方法就会被调用;
只要打印,__str__方法就会被调用,使用这个方法得到是人能理解的;
只要涉及显示与交互会话,__repr__ 方法就会被调用, 使用这个方法得到的编译器能理解的;
#|cs61a 课时笔记 对象的抽象
文章图片

True and false value
所有对象的布尔逻辑应该都是True,特殊方法 __bool__ 方法被定义返回True。用户自定义的类从布尔逻辑来说应该是True,但要实现这个, __bool__方法必须被重载。比如,我们认为银行账户的余额为0是False,需要给这个类添加一个 __bool__方法以实现这个行为。
>>> Account.__bool__ = lambda self: self.balance != 0

还可以直接调用bool构造器查看其逻辑值:
>>> bool(Account('Jack')) False >>> if not Account('Jack'): print('Jack has nothing') Jack has nothing

Sequence operations
__len__方法 【#|cs61a 课时笔记 对象的抽象】常使用len来求一个序列的长度,如:
>>> len('Go Bears!') 9

这是因为所有的内建序列类型都使用了__len__方法。上面的操作等价于下面的操作:
>>> 'Go Bears!'.__len__() 9

当没有提供__bool__方法时,序列的布尔逻辑值由长度决定。长度为0是为false,不为0时为true。
>>> bool('') False >>> bool([]) False >>> bool('Go Bears!') True

__getitem__方法 这个方法等价于一个元素选择器,也是可以被直接调用的。如下:
>>> 'Go Bears!'[3] 'B' >>> 'Go Bears!'.__getitem__(3) 'B'

Callable objects
在Python中,函数是一级对象,因此它们可以作为数据传递,并具有类似于任何其他对象的属性。Python还允许我们通过包含一个call方法来定义类似于“调用”函数的对象。使用此方法,我们可以定义一个行为类似于高阶函数的类。
如下例子,这是一个高阶函数:
>>> def make_adder(n): def adder(k): return n + k return adder >>> add_three = make_adder(3) >>> add_three(4) 7

可以创建一个类,定义一个__call__方法达到一样的效果:
>>> 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

data和function的分界还可以变得更加模糊。
Arithmetic
特殊方法还可以将定义内置运算符的行为用于用户定义对象的。为了提供这种通用性,Python遵循特定的协议来应用每个操作符。例如,要计算包含+运算符的表达式,Python会检查表达式的左操作数和右操作数上是否都有特殊方法。首先,Python在左操作数的值上检查add方法,然后在右操作数的值上检查radd方法。如果找到其中一个,则调用该方法,并将另一个操作数的值作为其参数。
Multiple Representations 这里不适用Python内部定义的复数,而是自己实现一个复数类型,并实现复数的加法和乘法。
首先定义一个Number类,然后使用通过add和mul方法抽象出该类的实例是怎么相加和相乘的。这个类没有__init__方法,它被设计为不能直接实例化,而是作为特殊数值类型的超类,比如复数。
>>> class Number: def __add__(self, other): return self.add(other) def __mul__(self, other): return self.mul(other)

复数有两种表达方式:(1)使用实部和虚部表示;(2)使用模和角度表示。
复数加法的实现使用方式(1)的表达更易求得:复数实部相加,虚部相加;
复数乘法的实现使用方式(2)的表达更易求得:将一个复数的长度乘以另一个复数的长度,然后将其旋转穿过另一个复数的角度得到的向量。
定义一个继承自Number的类Complex 类表示复数,并描述复数的运算如下:
>>> 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)

上面的代码中假定存在两个类:
  • 通过实部和虚部对构造虚数ComplexRI类;
  • 通过长度和角度构造虚数的ComplexMA类。
两个或多个属性值之间保持固定关系的要求是一个新问题。一种解决方案是只为一个表示存储属性值,并在需要时计算另一个表示。Python有一个从零参数函数动态计算属性的简单特性。@property修饰器允许在不使用调用表达式语法(表达式后面的括号)的情况下调用函数。
下面实现ComplexRI类存储real和imag属性,并根据需要计算大小和角度:
>>> 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)>>> ri = ComplexRI(5, 12) >>> ri.real 5 >>> ri.magnitude 13.0 >>> ri.real = 9 >>> ri.real 9 >>> ri.magnitude 15.0

同理,通过模和角度定义ComplexMA类,也能实现相互转化:
>>> 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)>>> ma = ComplexMA(2, pi/2) >>> ma.imag 2.0 >>> ma.angle = pi >>> ma.real -2.0

最终能够实现两个虚数的加法和乘法:
>>> from math import pi >>> ComplexRI(1, 2) + ComplexMA(2, pi/2) ComplexRI(1, 4) >>> ComplexRI(0, 1) * ComplexRI(0, 1) ComplexMA(1, 1 * pi)

编码多个表示的接口方法具有吸引人的特性。每个表示的类可以单独开发;它们只能在共享属性的名称以及这些属性的任何行为条件上达成一致。如果另一个程序员想在同一个程序中添加第三个复数表示,他们只需要创建另一个具有相同属性的类。
使用数据抽象,我们能够在不改变程序含义的情况下改变数据类型的实现。通过接口和消息传递,我们可以在同一个程序中有多个不同的表示。在这两种情况下,一组名称和相应的行为条件定义抽象使得类的定义变得灵活。
Generic Functions
泛型函数是应用于不同类型参数的方法或函数。上面的例子中Complex.add方法是通用的,因为它可以将ComplexRI或ComplexMA作为other的值。这种灵活性是通过确保ComplexRI和ComplexMA共享一个接口而获得的。使用接口和消息传递只是实现泛型函数的几种方法之一。另外两个问题:类型分派( type dispatching)和类型强制(type coercion)。
假设除了复数类之外,我们还可以实现了一个有理类来精确地表示分数:
>>> 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和mul方法实现了超类Number的接口。因此,我们可以使用熟悉的运算符来加和乘有理数。
>>> Rational(2, 5) + Rational(1, 10) Rational(1, 2) >>> Rational(1, 4) * Rational(2, 3) Rational(1, 6)

Type dispatching 那么怎么实现复数加上有理数这种跨类型操作?为了不严重违反抽象障碍的情况下支持它。我们希望能够向有理数添加一个复数,并且我们希望使用一个泛型的add方法来这样做,并希望这样能够正确地处理所有数值类型。
我们知道isinstance方法可以得到当前对象是否属于某个类:
>>> c = ComplexRI(1, 1) >>> isinstance(c, ComplexRI) True >>> isinstance(c, Complex) True >>> isinstance(c, ComplexMA) False

简单实用一个is_real函数就能实现类型分派( type dispatching):
>>> 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

还可以实用一个类型标签的属性,把复数和分数分开:
>>> 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

由于分数可以转为浮点数,复数和分数的加可以这么算:
>>> def add_complex_and_rational(c, r): return ComplexRI(c.real + r.numer/r.denom, c.imag)

复数在复平面中,模总是一个正值,角度为0表示整数,角度为pi表示负数。因此,复数与分数的乘法可以这么算:
>>> 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)

最后重写Number超类,__add____mul__方法中能够实现类型分派。需要类型标签属性type_tag实现分派。原理如下:如果两个相加的参数type_tag是一样的,直接相加,如果不一样,则要查看是否有两种不同的类型相加的定义,这里的定义了一个adders做怎样的事情。对于乘法,同理。
实现如下:
>>> 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) def cross_apply(self, other, cross_fns): cross_fn = cross_fns[(self.type_tag, other.type_tag)] return cross_fn(self, other) 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)

online python tutor 在线调试:这里
完整代码:
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)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) def cross_apply(self, other, cross_fns): cross_fn = cross_fns[(self.type_tag, other.type_tag)] return cross_fn(self, other) 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}class Complex(Number): type_tag = "com" 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)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)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)from fractions import gcd class Rational(Number): def __init__(self, numer, denom): g = gcd(numer, denom) self.type_tag = "rat" 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)print(ComplexRI(1.5, 0) + Rational(3, 2)) print(Rational(-1, 2) * ComplexMA(4, pi/2))

coercion
在完全不相关操作作用于完全不相关类型的一般情况下,实现显式的跨类型操作(尽管可能很麻烦)是我们所能期望的最好的方法。幸运的是,我们有时可以利用类型系统中潜在的附加结构来做得更好。通常不同的数据类型并不是完全独立的,可能有一些方法可以将一种类型的对象视为另一种类型的对象。这个过程叫做强迫。例如,如果要求我们将有理数与复数进行算术组合,我们可以将有理数视为虚数部分为零的复数。这样做之后,我们可以使用Complex.add和Complex.mul来组合它们。
一般来说,我们可以通过设计强制函数来实现这一思想,强制函数将一种类型的对象转换为另一种类型的等效对象。下面是一个典型的强制函数,它将有理数转换为具有零虚部的复数:
>>> def rational_to_complex(r): return ComplexRI(r.numer/r.denom, 0)

Number类的替代定义通过尝试将两个参数强制为同一类型来执行跨类型操作。强制字典通过一对类型标记索引所有可能的强制。
>>> 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}

与定义显式跨类型操作的方法相比,我们只需要为每对类型编写一个函数,而不是为每一组类型和每个泛型操作编写一个不同的函数。。
一些更复杂的强制方案不只是尝试将一种类型强制为另一种类型,而是尝试将两种不同的类型分别强制为第三种公共类型。考虑一个菱形和一个矩形:两者都不是另一个的特例,但都可以看作四边形。
强制的另一个扩展是迭代强制,其中一个数据类型通过中间类型强制转换为另一个数据类型。假设整数可以转换为实数,首先将其转换为有理数,然后将该有理数转换为实数。以这种方式链接强制可以减少程序所需强制函数的总数。
但是强制函数在应用时可能会丢失信息。在上述例子中,有理数是精确的表示,但是当它们被转换成复数时就变成了近似值。
参考:http://composingprograms.com/pages/27-object-abstraction.html

    推荐阅读