传送门
目录
一、继承
1.1什么是继承
在上一章我们讲了python面向对象的基本概念以及封装、继承、多态三大特性中的封装,这个部门我们来说继承和多态。
还是举个栗子来引入继承。
猫和狗都属于动物。动物都会吃饭和喝水,也会跑,那么我们定义一个动物类,并且定义吃饭、喝水、跑这三种方法,现在我们需要定义猫类和狗类,我们照例也需要给猫类狗类定义吃饭、喝水、跑这三种方法。大家发现问题没,那如果我现在需要继续定义兔子,老虎,鸟,狮子等等几十个类,那就要复制粘贴几十中方法写到每个类中,费时费力,代码也看着头疼。
这个时候我们就用到继承机制,继承机制经常用于创建和现有类功能类似的新类,又或是新类只需要在现有类基础上添加一些成员(属性和方法),但又不想直接将现有类代码复制给新类。也就是说,通过使用继承这种机制,可以轻松实现类的重复使用。就像父母和孩子一样,你有父母的共同特征,所以,像动物类,我们称之为父类,猫、狗这些继承动物类的,我们称之为子类。
子类继承父类时,只需在定义子类时,将父类(可以是多个)放在子类之后的圆括号里即可。语法格式如下:
class 类名(父类1, 父类2, ...):
#类定义部分
如果该类没有显式指定继承自哪个类,则默认继承 object 类(object 类是 Python 中所有类的父类,即要么是直接父类,要么是间接父类)。另外,Python 的继承是多继承机制,即一个子类可以同时拥有多个直接父类。
以下例子中,Person同时继承People类和Animal类,所以同时有say方法和display方法。
class People:
def say(self):
print("我是一个人,名字是:",self.name)
class Animal:
def display(self):
print("人也是高级动物")
#同时继承 People 和 Animal 类
#其同时拥有 name 属性、say() 和 display() 方法
class Person(People, Animal):
pass
zhangsan = Person()
zhangsan.name = "张三"
zhangsan.say()
zhangsan.display()
#输出结果
我是一个人,名字是: 张三
人也是高级动物
题外话:多继承的问题
事实上,大部分面向对象的编程语言,都只支持单继承,即子类有且只能有一个父类。而 Python 却支持多继承。我自己的体验是当时学java时觉得java的继承很清晰,代码也很容易看,而python的多继承有时候我会看着不习惯。和单继承相比,多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用。并且,和单继承相比,使用多继承经常需要面临的问题是,多个父类中包含同名的类方法。
对于这种情况,Python 的处置措施是:根据子类继承多个父类时这些父类的前后次序决定,即排在前面父类中的类方法会覆盖排在后面父类中的同名类方法。
比如刚才那个例子,如果Animal中也有一个say方法,那么实际调用的是People里面的。
建议:虽然python支持多继承,但日常生活中我们还是尽量使用单继承,这样可以保证编程思路更清晰,也可以避免不必要的麻烦。
1.2重写
刚才在介绍继承的时候,可能已经有读者发现问题了,子类继承父类,即继承父类中的方法,那如果子类需要继承父类,但父类中有些方法不适用,难道就不能继承了吗?
其实也可以,比如我们写一个鸟类,鸟有翅膀,会飞,但是世界之大无奇不有,比如鸵鸟,有翅膀,但不会飞,这个时候我们就要重写父类方法。
重写即子类从父类继承得来的类方法中,大部分是适合子类使用的,但有个别的类方法,并不能直接照搬父类的,如果不对这部分类方法进行修改,子类对象无法使用。针对这种情况,我们就需要在子类中重复父类的方法。
关于鸵鸟这个我们来写个代码:
class Bird:
#鸟有翅膀
def isWing(self):
print("鸟有翅膀")
#鸟会飞
def fly(self):
print("鸟会飞")
class Ostrich(Bird):
# 重写Bird类的fly()方法
def fly(self):
print("鸵鸟不会飞")
# 创建Ostrich对象
ostrich = Ostrich()
#调用 Ostrich 类中重写的 fly() 类方法
ostrich.fly()
#运行结果
鸵鸟不会飞
可以看到,因为 Ostrich 继承自 Bird,因此 Ostrich 类拥有 Bird 类的 isWing() 和 fly() 方法。其中,isWing() 方法同样适合 Ostrich,但 fly() 明显不适合,因此我们在 Ostrich 类中对 fly() 方法进行重写。 在重写后Ostrich调用的是重写之后的 fly() 类方法。
好了,这个看似棘手的问题解决了,现在又出现一个新的问题,如果我们在子类中重写了从父类继承来的类方法,那么当在类的外部通过子类对象调用该方法时,Python 总是会执行子类中重写的方法。 那我们想调用父类中这个被重写的方法,怎么调用呢?
回答这个问题前,我们先介绍一个前面一直没有说但一直在用的概念。全局变量和局部变量,全局变量就是在函数外部定义的变量。所有函数内部都可以使用这个变量。局部变量是在函数内部定义的变量。这个变量只能在定义这个变量的函数内部使用。
一个python程序是一个全局空间,其中的类就相当于一个类空间,类空间里面的方法相当于全局空间的函数。那么刚才那个问题就是在全局空间中,调用类空间中的函数,此时我们只需要在调用该函数时备注类名即可。
class Bird:
#鸟有翅膀
def isWing(self):
print("鸟有翅膀")
#鸟会飞
def fly(self):
print("鸟会飞")
class Ostrich(Bird):
# 重写Bird类的fly()方法
def fly(self):
print("鸵鸟不会飞")
# 创建Ostrich对象
ostrich = Ostrich()
#调用 Bird 类中的 fly() 方法
Bird.fly(ostrich)
#运行结果
鸟会飞
通过类名调用实例方法的这种方式,又被称为未绑定方法。即使用类名调用其类方法,Python 不会为该方法的第一个 self 参数自定绑定值,因此采用这种调用方法,需要手动为 self 参数赋值。
注意和上面对比学习。
同样的,如果有构造函数,也会调用从 People 继承来的构造函数。
class People:
def __init__(self,name):
self.name = name
def say(self):
print("我是人,名字为:",self.name)
class Animal:
def __init__(self,food):
self.food = food
def display(self):
print("我是动物,我吃",self.food)
#People中的 name 属性和 say() 会遮蔽 Animal 类中的
class Person(People, Animal):
pass
per = Person("zhangsan")
per.say()
#per.display()
#运行结果
我是人,名字为: zhangsan
如果此时把per.display()的注释去掉,运行这行代码,则会进行报错 ,如图:
原因是从 Animal 类中继承的 display() 方法中,需要用到 food 属性的值,但由于 People 类的构造方法“遮蔽”了Animal 类的构造方法,使得在创建 per 对象时,Animal 类的构造方法未得到执行,所以程序出错。
面对这种情况,正确的做法是定义 Person 类自己的构造方法(等同于重写第一个直接父类的构造方法)。但如果在子类中定义构造方法,则必须在该方法中调用父类的构造方法。
在子类的构造方法中调用父类的构造方法有两种办法,一种就是在类的外部调用其中的实例方法,可以像调用普通函数那样,只不过需要额外备注类名,(上文提到的),另一种就是使用 super() 函数。接下来我们简单介绍一下super()函数。
1.3 super()函数
super函数格式:
super().__init__(...)
-
调用父类的方法: 当子类继承自父类,并且想要在子类中调用父类的方法时,可以使用
super()
。这样可以确保在类继承结构中正确地调用相应的方法。 -
多重继承中的调用: 在多重继承的情况下,
super()
可以确保按照继承结构的顺序正确地调用相应的方法,以避免冲突和混淆。
咱们一个一个来说:
1.3.1调用父类的方法
当子类继承自父类,并且想要在子类中调用父类的方法时,可以使用 super()
。这样可以确保在类继承结构中正确地调用相应的方法。
class Animal(object):
def __init__(self, name):
self.name = name
def greet(self):
print('Hello, I am %s.' % self.name)
class Dog(Animal):
def greet(self):
super(Dog, self).greet() #调用父类Animal的greet方法
print('WangWang...')
d=Dog("xiaohuang")
d.greet()
#输出结果
Hello, I am xiaohuang.
WangWang...
在上面,Animal 是父类,Dog 是子类,我们在 Dog 类重定义了 greet 方法,为了能同时实现父类的功能,我们又调用了父类的方法 。
1.3.2多重继承中的调用(硬核)
ps:对于初学者而言,可以先往后学,这一块涉及到一些底层原理,可以扫一眼,听不懂其实对于日常初级编程,没有很大影响,本来多重继承就让人晕晕乎乎,也不常用。感兴趣的可以看一看。
class Base(object):
def __init__(self):
print("enter Base")
print("leave Base")
class A(Base):
def __init__(self):
print("enter A")
super(A,self).__init__()
print("leave A")
class B(Base):
def __init__(self):
print("enter B")
super(B,self).__init__()
print("leave B")
class C(A,B):
def __init__(self):
print("enter C")
super(C,self).__init__()
print("leave C")
c=C()
#输出结果
enter C
enter A
enter B
enter Base
leave Base
leave B
leave A
leave C
可能看到这个大家已经开始头疼了。实际就是C类继承自A,B,而A和B又分别继承Base类,每一个类的构造函数分别被调用了一次。
这个栗子其实很好说明了一个问题,那就是super 和父类其实没有实质性的关联,不然enterA后的输出结果应该是enter Base,而不是enter B,(Base是A的父类)
想要搞清楚这个问题,我们应该了解一下super的原理。
1.3.3 MRO列表
首先介绍一下这个东西,在python中,对于你定义的每一个类,Python 会计算出一个方法解析顺序(Method Resolution Order, MRO)列表,它代表了类继承的顺序,我们可以使用下面的方式获得C类的 MRO 列表:
print(C.mro())
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,
<class '__main__.Base'>, <class 'object'>]
这个列表真实的列出了类C的继承顺序。C->A->B->Base->object(前面我们提到过,object是所有类的父类)。在方法调用时,是按照这个顺序查找的。
那这个 MRO 列表的顺序是怎么定的呢,它其实是通过一个 C3 线性化算法来实现的,这就越说越深了,为了不跑题,也不打扰大家学python的信心,这个就不说了,感兴趣的读者可以自己去了解一下,总的来说,一个类的 MRO 列表就是合并所有父类的 MRO 列表,并遵循以下三条原则:
- 子类永远在父类前面
- 如果有多个父类,会根据它们在列表中的顺序被检查
- 如果对下一个类存在两个合法的选择,选择第一个父类
1.3.3 super的工作原理
现在我们说super的工作原理:
def super(cls, inst):
mro = inst.__class__.mro()
return mro[mro.index(cls) + 1]
代码中,cls 代表类,inst 代表实例 ,那么该代码作用是什么呢?首先获取 inst 的 MRO 列表,然后查找 cls 在当前 MRO 列表中的 index, 并返回它的下一个类,即 mro[index + 1]当你使用 super(cls, inst) 时,Python 会在 inst 的 MRO 列表上搜索 cls 的下一个类。
那么,在刚才的栗子中,类 C 的 init 方法是super(C,self).__init__()。这里的self是C的实例。
MRO结果是:[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>]
所以当C执行完后,会执行A的init,并打印enter A,并执行super(A,self).__init__()。此时这个self也是C的实例,MRO 列表跟上面是一样的,搜索 A 在 MRO 中的下一个类,即B,于是就开始打印enter B。这也就解释了为什么不是打印enter Base。
二、多态
多态指的是一类事物有多种形态,一个抽象类有多个子类(因而多态的概念依赖于继承),不同的子类对象调用相同的方法,产生不同的执行结果,多态可以增加代码的灵活度
多态要满足这两个特性:
- 继承:多态一定是发生在子类和父类之间;
- 重写:子类重写了父类的方法。
class Csdn: def say(self): print("Csdn类的say方法") class CPython(Csdn): def say(self): print("CPython类的say方法") class Cjava(Csdn): def say(self): print("Cjava类的say方法") a = Csdn() a.say() a = CPython() a.say() a = Cjava() a.say() Csdn类的say方法 CPython类的say方法 Cjava类的say方法
多态的意义:
对于一个变量,我们只需要知道他是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法(调用方只管调用,不管细节)
当需要新增功能,只需要新增一个Animal的子类实现run()方法,就可以在原来的基础上进行功能扩展,这就是著名的“开放封闭”原则。
“开放封闭”原则:
对扩展开放:允许新增Animal子类
对修改封闭:不需要修改依赖Animal类型的run()等函数。
三、抽象类和接口
抽象类是一种不能被实例化的类,它只能被用作其他类的基类。抽象类中可以定义抽象方法,但是不能实现它们。接口是一种只包含抽象方法的类,它用于定义类之间的通信协议。
抽象类不能被实例化,只能被继承,且子类必须实现抽象方法。
默认情况下,Python 不提供抽象类。 Python 附带一个模块,该模块为定义抽象基类(ABC)提供了基础,该模块名称为 ABC。ABC 的工作方式是将基类的方法装饰为抽象,然后将具体类注册为抽象基的实现。当使用关键字@abstractmethod 修饰时,方法变得抽象。
举个例子:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
d = Dog()
c = Cat()
print(d.speak())
print(c.speak())
#输出结果
Woof!
Meow!
为什么要用抽象类:
一个抽象类可以被认为是其他类的蓝图。它允许您创建一组必须在从抽象类构建的任何子类中创建的方法。包含一个或多个抽象方法的类称为抽象类。抽象方法是具有声明但没有实现的方法。在设计大型功能单元时,我们使用抽象类。当我们想为组件的不同实现提供通用接口时,我们使用抽象类。
标签:Python,子类,self,print,笔记,面向对象,继承,父类,方法 From: https://blog.csdn.net/qq_45951644/article/details/139415548