在定义一组类的继承层次结构时,避免重复代码的技术之一是设计一个基类,该基类可以被需要他的其他类所继承。然而,如果以各类唯一目的就是作为继承的基类,这个类既可以叫做抽象基类。更正式地说,一个抽象类不能直接实例化,因为它可以是没有任何实际意义的类。因为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]]
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=[[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的内置类为许多操作提供了自然的语义。比如,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__的区别也允许类根据情况定义不同的语义,如操作数在矩阵乘法中就是不可交换的。
到此为止,面对对象编程的细则已经给大家补充了很多了,后面我们再介绍数据结构时可能还会补充另外一些细则,大家记得追更~