数据结构|面对对象编程细则(三)


面对对象编程补充(3)

  • 抽象基类
  • 深拷贝和钱拷贝
    • 浅拷贝
    • 深拷贝
    • 二维列表的创建
  • 运算符重载和python的特殊方法

前面两篇内容,我们已经介绍了很多实用的面对对象编程技巧,这一节我们继续补充一些概念。对于这些概念大多只需要我们知道即可:
抽象基类 【数据结构|面对对象编程细则(三)】在定义一组类的继承层次结构时,避免重复代码的技术之一是设计一个基类,该基类可以被需要他的其他类所继承。然而,如果以各类唯一目的就是作为继承的基类,这个类既可以叫做抽象基类。更正式地说,一个抽象类不能直接实例化,因为它可以是没有任何实际意义的类。因为python由于对声明类型没有做强制要求,这种多态性也自然让python对定义正式的抽象基类没有强烈的要求。也因此我们编程中,对抽象基类的应用也不多见。但是python依然提供了一个abc模块定义了抽象基类:
数据结构|面对对象编程细则(三)
文章图片

我们来看一个例子:
from abc import abstractmethod,ABCMeta class Seq(metaclass=ABCMeta): @abstractmethod def __len__(self): '''返回接受列表的长度'''

在这个例子中,我们定义了一个__len__()方法,可以看到我们注释了该方法所实现的功能,然而其内部却没有任何实现。这是因为在该方法声明前,我们使用了@abstractmethod声明了这个方法是抽象的,不需要再Seq类中提供实现,我们希望这个方法在继承该类的子类中提供实现。
深拷贝和钱拷贝 拷贝是一个非常常见的内容,在C语言中,我们只需要定义两个不同的变量名,再将一个标识符内容直接复制给另一个,就可以较好地完成拷贝,然而在python基础补充中我们提到过,如果按以下定义:
a=[1,2,3,4,5] b=a # a和b的实际地址是一样的,只是给一个内容起了两个别名, # 并不是创立一个新的列表 b.append(6) print(a) # 输出为:[1, 2, 3, 4, 5, 6]

以上特点还可以继续拓展到二维列表的创建中。假如我们使用a=[[0]*n]*m的格式创建一个列表,然后赋值,会是什么结果呢:
a=[[0]*3]*3 for i in range(3): a[0][i]=i+1 print(a) # 输出为:[[1, 2, 3], [1, 2, 3], [1, 2, 3]]

虽然我们的指令是更改a[0]列表的内容,但是后面的列表也都跟着联动了。问题出现在这种创建二维列表的方式上,实际上a[0],a[1],a[2]并非三个列表,而只是一个列表的三个别称而已,因此改动一个,另外的两个也会跟着动。以上两个例子,都没有达到拷贝的效果,如果放在实际的编程中,会造成莫名其妙的错误。那么,python中如何拷贝一个列表呢?
浅拷贝 浅拷贝的表达方式很简单,我们只需要做以下操作:
a=[1,2,3,4,5] b=list(a) b.append(6) print(a) # 输出为:[1, 2, 3, 4, 5]

看起来这样就满足要求了。其原理如下:
数据结构|面对对象编程细则(三)
文章图片

这是一维列表的情况,看起来已经满足了我们的要求。下面我们拟想另一种情况,我们建立一个二维列表,每个元素存储一个学生的三科成绩,并且不需要注明是哪位学生的成绩:
a=[[70,65,92],[88,84,73],[60,60,60]] b=list(a)

大家猜猜拷贝的情况是什么样呢?
数据结构|面对对象编程细则(三)
文章图片

如果我们想要给b列表再加一列,是不会影响a的:
a=[[70,65,92],[88,84,73],[60,60,60]] b=list(a) b.append([62,100,91]) print(a,'\n',b) # 输出为:[[70, 65, 92], [88, 84, 73], [60, 60, 60]] #[[70, 65, 92], [88, 84, 73], [60, 60, 60], [62, 100, 91]]

原因如下:
数据结构|面对对象编程细则(三)
文章图片

因此,如果我们通过以下方式修改b[0]的内容:
a=[[70,65,92],[88,84,73],[60,60,60]] b=list(a) b.append([62,100,91]) del (b[0])[1] # 删除b[0]列表中的第二个元素 b[0].insert(1,72) # 将b[0]列表中第二个元素改为72 # 以上两行也可以直接写成: # b[0][1]=72 print(a,'\n',b) # 输出为:[[70, 72, 92], [88, 84, 73], [60, 60, 60]] #[[70, 72, 92], [88, 84, 73], [60, 60, 60], [62, 100, 91]]

就会发现,通过b修改a和b列表重叠部分的元素时,依然会对a造成影响。
如果想要从根本上解决这个问题,就需要用到深拷贝。
深拷贝 python给我们提供了一个拷贝的模块即copy,这个模块可以帮助我们实现真正的拷贝即深拷贝。深拷贝可以让上例a和b成为完全独立的部分:
import copy a=[[70,65,92],[88,84,73],[60,60,60]] b=copy.deepcopy(a) b.append([62,100,91]) b[0][1]=72 print(a,'\n',b) # 输出为:[[70, 65, 92], [88, 84, 73], [60, 60, 60]] #[[70, 72, 92], [88, 84, 73], [60, 60, 60], [62, 100, 91]]

此时a和b的关系就是独立的:
数据结构|面对对象编程细则(三)
文章图片

二维列表的创建 说完了一维列表的拷贝,下面我们来介绍上文中遗留的另一个问题——二维列表的初始化。如果想要创建一个后期能够独立而非联动的修改具体值的二维列表,需要用以下方式:
a=[[0]*m for i in range(n)]

这样就可以让a[0]到a[n-1]是n个独立的列表啦:
a=[[0]*3 for i in range(5)] for i in range(3): a[3][i]=i+1 print(a) # 输出为:[[0, 0, 0], [0, 0, 0], [0, 0, 0], [1, 2, 3], [0, 0, 0]]

当然,创建一个联动的二维列表在某些特定情况下也是有用的。
运算符重载和python的特殊方法 Python的内置类为许多操作提供了自然的语义。比如,a+b语句可以调用数值类型语句,也可以连接序列类型。当定义一个新类时,我们必须考虑到当a或者b是类中的实例时是否应该定义类似于a+b的语句。
默认情况下,对于新的类来说,“+”操作符是未定义的。然而,类的作者可通过操作符重载(operator overloading)技术来定义它。这个定义可通过一个特殊的命名方法来实现。特别的是,名为__add__的方法重载+操作符,__add__用右边的操作作为参数并返回表达式的结果。也就是说,a+b语句,被转换为一个调用a.__add(b)对象的方法。类似的特殊命名方法存在其他操作符中。表2-1提供了与这一方法类似的完整列表。
表2-1 用Python特殊方法实现的重载操作
常见语法 特别方法的形式
a+b a._add _(b)或 b._radd _(a)
a-b a._sub _(b)或b._rsub _(a)
a*b a._mul _(b)或b._rmul _(a)
a/b a._truediv _(b)或b._rtruediv _(a)
a//b a._floordiv _(b)或b._rfloordiv _(a)
a%b a.__ mod_ _(b); 或b._rmod _(a)
a**b a._pow _(b)或b._rpow _(a)
a< a._lshift _(b); 或b._rlshift _(a)
a>>b a._rshift _(b); 或b._rrshift _(a)
a&b a._and _(b); 或b._rand _(a)
a^b a._xor _(b); 或b._rxor _(a)
a b
a+=b a._iadd _(b)
a-=b a._isub _(b)
a*=b a._imul _(b)
+a a._pos _(b)
-a a._neg _(b)
~a a._inwert _(b)
abs(a) a._abs _(b)
a a._lt _(b)
a<=b a._le _(b)
a>b a._gt _(b)
a>=b a._ge _(b)
a==b a._eq _(b)
a!=b a._ne _(b)
vina a._contains _(v)
a[k] a._getitem _(k)
a[k]=v a._setitem _(k,v)
dela[k] a._delitem _(k)
a(arg1,arg2,…) a._call _(arg1,arg2,…)
len(a) a._len _()
hash(a) a._hash _()
iter(a) a._iter _()
next(a) a._next _()
bool(a) a._bool _()
float(a) a._float _()
int(a) a._int _()
repr(a) a._repr _()
reversed(a) a._reversed _()
str(a) a._str _()
当一个二元操作符应用于两个不同类型的实例中时,Python对根据左操作数的类进行判断。在这个例子中,对于使用__mul__方法把字符串与实例相乘,可以通过检查int类是否提供了相应的定义。然而,如果这个类没有实现这一行为,Python就会以一种名为 rmul(即“右乘”)的特殊方法来检查右操作数的类的定义。该方法为新用户定义的类提供了一个支持包含已存在类(所给的已存在的类可能没有定义引用该新类的行为)的实例的混合操作的方法。__mul__和__rmul__的区别也允许类根据情况定义不同的语义,如操作数在矩阵乘法中就是不可交换的。
到此为止,面对对象编程的细则已经给大家补充了很多了,后面我们再介绍数据结构时可能还会补充另外一些细则,大家记得追更~

    推荐阅读