类的三大特征之——继承
昨天我们学习了面向对象编程、类与对象。昨天遗漏了一个小知识点就是:在python中,一切皆对象。
首先,我们要来说明,在python3中,统一了类与类型的概念,即在python3中,类就是类型,类型就是类。
我们通过type来查看一个数据的类型,我们也可以通过type来查看一个对象的类型,然后我们发现,他们的类型都是class(类)依旧是说,我们平时使用的所有数据类型,列表啊,字典啊等等都是一个对象,由类产生的对象。
那么现在再去理解对象这个东西会不会更好理解了一些呢,对象不仅得到了数据,还得到了对数据进行操作的一些方法,例如:获取一个列表对象后,其下的一些方法如append等。
今天我们来说一说类的三大特征之一:继承
首先,什么是继承
继承的意思想必大家都知道,我们疑惑的是,在python中,继承是什么,它和现实中的继承有没有区别。那么,首先我要告诉你,在python中,继承是类的独有的方法。继承是创建类的一种方式。python支持多继承,也就是说,一个类可以继承任意数量的父类。父类又称为基类或超类,新建的类称为子类或派生类。
为什么要有继承
继承可以减少代码冗余。(后面会讲到为什么继承可以减少代码冗余)
怎么用继承
昨天我们讲定义一个类是通过class 类名: 这样去定义的。在定义类的时候,也是可以加括号的,括号内写入一个或多个类名,这样就是新建的类继承了括号内的类。
我们用代码来示例一下:
class A: # 定义一个类A
name = 'A' # 类A中有一个属性叫name = 'A'
class B: # 定义一个类B
age = 18 # 类B中有一个属性叫age = 18
class C(A): # 定义一个类C,继承类A
pass
class D(A, B): # 定义一个类D,同时继承类A与类B
pass
obj1 = C() # 生成一个类C的对象
print(obj1.name)
我们昨天讲过一个属性的查找,我们来看,对象obj内没有name这个属性,类C中也没有name属性。类C的父类A中有name属性,所以通过obj也可以查找到name这个属性。讲到这里大家应该基本可以明白了吧。继承就是:子类会“”遗传”父类的属性。
那么我们应该如何在代码中定义一个父类/基类呢?我们可以通过现实中的例子来看我们应该如何定义出一个父类
继承描述的是子类与父类之间的关系,是一种什么是什么的关系。要找出这种关系,必须先抽象再继承
我们继承的目的是为了减少代码冗余,也就是说,当两个类中出现了相同的代码,我们应该将相同的代码部分提取出来,定义为父类,然后再由这两个类去分别继承该父类。通俗点来说:父类的属性更普通,子类继承父类的普通属性以后,会定义自己与其他类不同的属性。我们定义父类的目的就是将子类与子类之间的相同代码写一遍,减少代码冗余。
代码示例:
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print('eatting')
def drink(self):
print('drinking')
class Cat(Animal):
def yell(self):
print('喵')
class Dog(Animal):
def yell(self):
print('汪')
类Cat和类Dog都有吃与喝的属性,都有名字和年龄的属性,所以我们把相同的部分拉出来,定义为父类Animal,这样子,我们就减少了代码冗余。
学会了类的继承,我们要关注一个重点:属性查找顺序
class Foo:
def f1(self):
print('Foo.f1')
def f2(self):
print('Foo.f2')
self.f1()
class Bar(Foo):
def f1(self):
print('Bar.f1')
b = Bar()
b.f2()
这里的b.f2()最终的执行结果是 Foo.f2 \n Bar.f1
这是非常重要的知识点:我们来按照顺序看一看,首先,我们通过类Bat获取一个对象b。通过对象b去调用f2这个绑定方法。先在对象自己的名称空间中查找,自己名称空间中没有则去类的名称空间中查找,类的名称空间中也没有,则去该类的父类的名称空间中查找,在父类的名称空间中,我们找到了f2,然后执行f2,这时,对象b是当做参数self传入进来,传入以后,执行f2函数,第一行打印Foo.f2,然后执行self.f1()也就是执行b.f1()再次从对象自己的名称空间中查找,没有则去类中查找,在类中我们就可以找到f1了。所以结果是上面所说的结果。大家一定要对属性的查找顺序铭记在心。这是非常重要的知识点。
当然子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了。
新式类与经典类概念:
python2与python3在继承上的区别:
新式类:但凡继承object类的子类,以及该子类的子子类,...都称之为新式类
经典类:没有继承object类的子类,以及该子类的子子类,...都称之为经典类
我们来想一下有没有这样一种场景:
父类中定义了一个属性,子类中需要给该属性增加一些东西,就目前的知识来说,我们只能重新在子类中定义,这样就又出现了重复代码,代码冗余又出现了,那么现在我们就来学习一下如何解决在子类中重用父类的属性
子类中重用父类属性有两种方式:
方式一:
class OldboyPeople:
school = 'Oldboy'
def __init__(self, name, age, gender): # 父类中定义init
self.name = name
self.age = age
self.gender = gender
# print(OldboyPeople.__init__)
class OldboyStudent(OldboyPeople): # 这个子类与父类的init完全一样,所以可以直接使用
# def __init__(self, name, age, gender):
# self.name = name
# self.age = age
# self.gender = gender
def choose_course(self):
print('%s is choosing course' % self.name)
class OldboyTeacher(OldboyPeople): # 该子类中的init比父类多了两个参数
def __init__(self, name, age, gender, level, salary):
# self.name = name
# self.age = age
# self.gender = gender
OldboyPeople.__init__(self, name, age, gender) # 直接调用父类下的init函数
# 注意这里是直接调用的函数,不再是绑定方法
self.level = level
self.salary = salary
def score(self, stu, num):
stu.num = num
print('老师%s给学生%s打分%s' % (self.name, stu.name, num))
方式一是指名道姓地访问某一个类中的函数,与继承没有太大的关系。
方式二:
class OldboyPeople: # 父类
school = 'Oldboy'
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
class OldboyTeacher(OldboyPeople):
def __init__(self, name, age, gender,level,salary):
# OldboyPeople.__init__(self, name, age, gender) # 这是方式一的用法
super(OldboyTeacher,self).__init__(name, age, gender)
# 在python3中,super函数不需要传递参数,为了学习所以将参数写上去了。
self.level=level
self.salary=salary
def score(self,stu,num):
stu.num=num
print('老师%s给学生%s打分%s' %(self.name,stu.name,num))
方式二:super(OldboyTeacher,self),在python3中super可以不传参数,调用该函数会得到一个特殊的对象,该对象是专门用来访问父类中属性。
强调:super会严格参照类的mro列表依次查找属性!!!
上面我们提到了一个叫mro列表的东西,那么它到底是个什么呢?听我给你慢慢道来
我们来详细讲一下类的继承中,属性的查找顺序!(重点)
在单继承背景下,无论是新式类还是经典类属性查找顺序都一样(上面提到过新式类与经典类的概念)先查看对象自己的名称空间,找不到再找类的名称空间,找不到再找父类的名称空间,这样依次找下去。
在Java和C#中子类只能继承一个父类,而Python中子类可以同时继承多个父类,如A(B,C,D),如果继承关系为非菱形结构,则会按照先找B这一条分支,然后再找C这一条分支,最后找D这一条分支的顺序直到找到我们想要的属性;如果继承关系为菱形结构,那么属性的查找方式有两种,分别是:深度优先和广度优先
深度优先:沿着一条支线走到头再走下一条支线
广度优先:沿着一条支线走到头的前一步,判断这个头是否还有别的支线没有走,直到该头的所有支线走完才会走这个头
python到底是如何实现继承的,对于你定义的每一个类,python会计算出一个方法解析顺序(mro)列表,这个mro列表就是一个简单的所有基类的线性顺序列表,mro列表是基于C3算法得到的。
为了实现继承,python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:
1.子类会先于父类被检查;
2.多个父类会根据它们在列表中的顺序被检查;
3.如果对下一个类存在两个合法的选择,选择第一个父类
在子类中重用父类属性的两种方法介绍完了,使用哪种方法都是可以的,但是!!!千万不要两种方法混合使用!!!
还有,在程序中,能不使用多继承就不要使用多继承!