python之路25 -- 之面向对象的三大特性(继承、多态、封装)
继承
1、什么是继承
继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称为派生类或子类
一个类:可以被多个类继承,也可以继承多个父类 -->python里特有的,别的语言里没有
python中类的继承分为:单继承和多继承
2、继承的语法
class Foo1: #定义父类1 pass class Foo2: #定义父类2 pass class Test1(Foo1): #单继承,当前类为:Test1,继承了Foo1类的属性 pass class Test2(Foo1,Foo2): #多继承,当前类为:Tese2,继承了Foo1类和Foo2类的属性 pass print(Test1.__bases__) #查看子类的父类,结果:(<class '__main__.Foo1'>,) print(Test2.__bases__) #查看子类的父类,结果:(<class '__main__.Foo1'>, <class '__main__.Foo2'>) print(Foo1.__bases__) #如果指定父类,python的类会默认继承object类,object是所有python类的父类
画图看懂类的继承

3、继承与抽象
抽象:即抽取类似或比较相像部分,
下面是一个人和狗的抽象图:

- 继承:是基于抽象的结果,通过编程语言去实现它,肯定是先经历抽象这个过程,才能通过继承的方式去表达出抽象的结构。
3.1、使用继承来解决代码重用的例子
==========================第一部分 例如 猫可以:吃、喝、爬树 狗可以:吃、喝、看家 如果我们要分别为猫和狗创建一个类,那么就需要为 猫 和 狗 实现他们所有的功能,伪代码如下: #猫和狗有大量相同的内容 class 猫: def 吃(self): # do something def 喝(self): # do something def 爬树(self): # do something class 狗: def 吃(self): # do something def 喝(self): # do something def 看家(self): #do something ==========================第二部分 上述代码不难看出,吃、喝是猫和狗都具有的功能,而我们却分别的猫和狗的类中编写了两次。如果使用 继承 的思想,如下实现: 动物:吃、喝 猫:爬树(猫继承动物的功能) 狗:看家(狗继承动物的功能) 伪代码如下: class 动物: def 吃(self): # do something def 喝(self): # do something # 在类后面括号中写入另外一个类名,表示当前类继承另外一个类 class 猫(动物): def 爬树(self): print '喵喵叫' # 在类后面括号中写入另外一个类名,表示当前类继承另外一个类 class 狗(动物): def 看家(self): print '汪汪叫' ==========================第三部分 #继承的代码实现 class Animal: def eat(self): print("%s 吃 " %self.name) def drink(self): print ("%s 喝 " %self.name) class Cat(Animal): def __init__(self, name): self.name = name self.breed = '猫' def climb(self): print('爬树') class Dog(Animal): def __init__(self, name): self.name = name self.breed='狗' def look_after_house(self): print('汪汪叫') # ######### 执行 ######### c1 = Cat('小白家的小黑猫') #实例化 c1.eat() #结果为:小白家的小黑猫 吃 c2 = Cat('小黑的小白猫') #实例化 c2.drink() #结果为:小黑的小白猫 喝 d1 = Dog('胖子家的小瘦狗') #实例化 d1.eat() #结果为:胖子家的小瘦狗 吃 d1.look_after_house() #结果为:汪汪叫
说明:
在开发程序的过程中,如果我们定义了一个类A,然后又想新建立另外一个类B,但是类B的大部分内容与类A的相同时
我们不可能从头开始写一个类B,这就用到了类的继承的概念。
通过继承的方式新建类B,让B继承A,B会‘遗传’A的所有属性(数据属性和函数属性),实现代码重用
- 提示:用已经有的类建立一个新的类,这样就重用了已经有的软件中的一部分设置大部分,大大生了编程工作量,这就是常说的软件重用,不仅可以重用自己的类,也可以继承别人的,比如标准库,来定制新的数据类型,这样就是大大缩短了软件开发周期,对大型软件开发来说,意义重大.
派生
当然子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了。
class Animal: #定义动物类 def __init__(self,name,aggr,hp): #初始化方法 self.name = name #初始化属性 self.aggr = aggr self.hp = hp def eat(self): #类方法 print("吃药回血") self.hp += 100
class Dog(Animal): #定义Dog类,继承了Animal类的属性 def __init__(self,name,aggr,hp,kind): #初始化方法,必须有父类(Animal)的所有属性,然后可以增加自己的派生属性 Animal.__init__(self,name,aggr,hp) #如果还需要继承父类的属性,需要是单独写上父类的初始化属性:父类.__init__(父类的初始化属性) self.kind = kind #派生属性 def eat(self): #Dog类的方法 Animal.eat(self) #如果既想实现新的功能也想使用父类的原本功能,还需要在子类中再调用父类的功能 self.teeth = 2 def bite(self,person): #派生方法 person.hp -= self.aggr
class Person(Animal): #定义Person类,继承Animal类 def __init__(self,name,aggr,hp,sex): #初始化方法,必须有父类(Animal)的所有属性,然后可以增加自己的派生属性 Animal.__init__(self,name,aggr,hp) #如果还需要继承父类的属性,需要单独写上父类的初始化属性:父类.__init__(父类的初始化属性) self.sex = sex #派生属性 def attack(self,dog): #派生方法 dog.hp -= self.aggr
jin = Dog('金老板',100,500,'吉娃娃') #实例化 alex = Person("alex",1,2,None) #实例化 jin.eat() #结果为:吃药回血 print(jin.hp) #结果为:600 alex.eat() #吃药回血 print(alex.hp) #结果为:102 jin.bite(alex) #狗咬了alex一口 print(alex.hp) #结果为:2
说明:
- 父类中没有的属性,在子类中出现,叫做派生属性
- 父类中没有的方法,在子类中出现,叫做派生方法
- 只要是子类的对象调用子类中的名字一定用子类的,子类中没有才找父类,如果父类中也没有就会报错
- 如果父类,子类都有相同的方法名时一定是用子类的,
- 如果必须想调用父类的方法名,需要增加单独调用,方法:父类名.方法名,需要自己传self参数
- super().方法,不需要自己传self
- 正常的代码中使用单继承-->减少了代码的重复就是继承要解决的问题
- 继承表达式是一种子类和父类的关系
画图说明(子类的对象调用子类中的名字一定用子类的):

2、supert类的派生
在子类中,新建的重名的函数属性,在编辑函数内功能的时候,有可能需要重用父类中重名的那个函数功能,应该是用调用普通函数的方式,即:类名.func(),此时就与调用普通函数无异了,因此即便是self参数也要为其传值.
在python3中,子类执行父类的方法也可以直接用super方法.
class Animal: def __init__(self,name,aggr,hp): self.name = name self.aggr = aggr self.hp = hp def eat(self): print("吃药回血") self.hp += 100 class Dog(Animal): def __init__(self,name,aggr,hp,kind): super().__init__(name,aggr,hp) #在一个子类里调用super()方法的意思为:找我的父类,然后就可以调用父类的方法了,调用的方法是传的参数不同,使用super方法时是:super(Dog,self).__init__(name,aggr,hp)这样的传参方法,super(这里的参数可以省略),super关键字只在新式类里才有,python3中所有的类都是新式类 self.kind = kind #派生属性 jin = Dog("金老板",200,500,'teddy') print(jin.name) #金老板 super(Dog,jin).eat() #super也可以再类外面使用,使用方法为:super(类名,依赖于该类的实例化名字).该类下的方法
画图看懂supert的执行步骤

类的多继承
1、多继承
class A: def func(self):print('A') class B: def func(self):print("B") class C: def func(self):print("C") class D(A,B,C): def func(self):print("D") d = D d.func() #实例化后的调用,首先到自己命名空间去找方法,如果没有的话回去离子类最近的父类(继承时最近的)里去找如果还没有就去离子类第二近的父类里去找,依次类推,如果所有父类里都没有才会报错
2、钻石继承
class A: def func(self):print('A') class B(A): def func(self):print("B") class C(A): def func(self):print("C") class D(B,C): def func(self):print("D") d = D d.func() #python3里的新式类默认:继承是以广度优先的,然后再深度,再python2里的经典类默认:继承是以深度优先的
多继承的总结:
- 多类继承中,我们子类的对象调用一个方法,默认是就近原则,找的顺序是(由近到远)
- 经典类:深度优先
- 新式类:广度优先
- python2.x:新式类和经典类共存
- python3.x:只有新式类,默认继承object
- 经典类和新式类还有一个区别:mro(记录类的继承顺序方法)方法只有再新式类中存在,super只有再python3中存在
- super的本质为:不是直接找父类,而是根据调用者
画图看懂多继承的继承顺序

接口类与抽象类
-
接口类:python原声不支持,(默认多继承),接口类的特点是:接口类中的所有方法都必须不能实现
-
抽象类:python原生支持的,(不支持多继承),抽象类的特点是:抽象类中方法可以有一些代码的实现
-
接口类和抽象类都是一种设计模式
继承有两种用途:
一:继承基类的方法,并且做出自己的改变或者扩展(代码重用)
二:声明某个子类兼容于某基类,定义一个接口类Interface,接口类中定义了一些接口名(就是函数名)且并未实现接口的功能,子类继承接口类,并且实现接口中的功能
1、接口类的例子
统一支付入口的例子:
如果定义了接口类,且其他类也继承了接口类,那么继承接口类的类没有接口类中的方法就会报错,所以说接口类是一种规范
from abc import abstractmethod,ABCMeta #接口类需要的模块 class Payment(metaclass=ABCMeta): #规范类,必须要继承metaclass=ABCMeta(默认元类类型) @abstractmethod #必须加@abstractmethod 装饰器 def pay(self,money): #没有实现这个方法就报错 raise NotImplemented #raise:主动抛出一个异常 class Wechat(Payment): #定义微信支付的类,继承Payment类 def pay(self,money): #pay方法 print("已经使用微信支付了%s元"%(money)) class Alipay(Payment): #支付宝支付,继承了Payment类 def pay(self,money): #pay方法 print("已经使用支付宝支付了%s元"%(money)) class Applepay(Payment): #苹果支付,继承了Payment类 def fuqian(self,money): #付钱方法,方法不统一 print("已经使用apple支付支付了%s元"%(money)) def pay(pay_obj,money): #定义了一个统一支付的入口 pay_obj.pay(money) wechat = Wechat() 实例化 ali = Alipay() #实例化 pay(ali,10) #结果为:已经使用支付宝支付了10元 app = Applepay #实例化applepay,报错了:app = Applepay(),原因是applepay里没有pay这个方法,所以报错了,解决办法就是将applepay里的支付方法改为和接口类里一样的方法
为什么要用接口类:
接口提取了一群类共同的函数,可以把接口当做一个函数的集合。 然后让子类去实现接口中的函数。 这么做的意义在于归一化,什么叫归一化,就是只要是基于同一个接口实现的类,那么所有的这些类产生的对象在使用时,从用法上来说都一样。 归一化,让使用者无需关心对象的类是什么,只需要的知道这些对象都具备某些功能就可以了,这极大地降低了使用者的使用难度。 比如:我们定义一个动物接口,接口里定义了有跑、吃、呼吸等接口函数,这样老鼠的类去实现了该接口,松鼠的类也去实现了该接口,由二者分别产生一只老鼠和一只松鼠送到你面前,即便是你分别不到底哪只是什么鼠你肯定知道他俩都会跑,都会吃,都能呼吸。 再比如:我们有一个汽车接口,里面定义了汽车所有的功能,然后由本田汽车的类,奥迪汽车的类,大众汽车的类,他们都实现了汽车接口,这样就好办了,大家只需要学会了怎么开汽车,那么无论是本田,还是奥迪,还是大众我们都会开了,开的时候根本无需关心我开的是哪一类车,操作手法(函数调用)都一样
2、抽象类
什么是抽象类
与java一样,python也有抽象类的概念但是同样需要借助模块实现,抽象类是一个特殊的类,它的特殊之处在于只能被继承,不能被实例化
为什么要有抽象类
如果说类是从一堆对象中抽取相同的内容而来的,那么抽象类就是从一堆类中抽取相同的内容而来的,内容包括数据属性和函数属性。
比如我们有香蕉的类,有苹果的类,有桃子的类,从这些类抽取相同的内容就是水果这个抽象的类,你吃水果时,要么是吃一个具体的香蕉,要么是吃一个具体的桃子。。。。。。你永远无法吃到一个叫做水果的东西。
从设计角度去看,如果类是从现实对象抽象而来的,那么抽象类就是基于类抽象而来的。
从实现角度来看,抽象类与普通类的不同之处在于:抽象类中有抽象方法,该类不能被实例化,只能被继承,且子类必须实现抽象方法。这一点与接口有点类似,但其实是不同的,即将揭晓答案
import abc #利用abc模块实现抽象类 class All_file(metaclass=abc.ABCMeta): all_type='file' @abc.abstractmethod #定义抽象方法,无需实现功能 def read(self): '子类必须定义读功能' with open('filaname') as f: pass @abc.abstractmethod #定义抽象方法,无需实现功能 def write(self): '子类必须定义写功能' pass class Txt(All_file): #子类继承抽象类,但是必须定义read和write方法 def read(self): print('文本数据的读取方法') def write(self): print('文本数据的读取方法') class Sata(All_file): #子类继承抽象类,但是必须定义read和write方法 def read(self): print('硬盘数据的读取方法') def write(self): print('硬盘数据的读取方法') class Process(All_file): #子类继承抽象类,但是必须定义read和write方法 def read(self): print('进程数据的读取方法') def write(self): print('进程数据的读取方法') wenbenwenjian=Txt() yingpanwenjian=Sata() jinchengwenjian=Process() #这样大家都是被归一化了,也就是一切皆文件的思想 wenbenwenjian.read() yingpanwenjian.write() jinchengwenjian.read() print(wenbenwenjian.all_type) print(yingpanwenjian.all_type) print(jinchengwenjian.all_type)
抽象类与接口类:
抽象类的本质还是类,指的是一组类的相似性,包括数据属性(如all_type)和函数属性(如read、write),而接口只强调函数属性的相似性。
抽象类是一个介于类和接口直接的一个概念,同时具备类和接口的部分特性,可以用来实现归一化设计
在python中,并没有接口类这种东西,即便不通过专门的模块定义接口,我们也应该有一些基本的概念。
多继承问题:
在继承抽象类的过程中,我们应该尽量避免多继承;
而在继承接口的时候,我们反而鼓励你来多继承接口
接口隔离原则: 使用多个专门的接口,而不使用单一的总接口。即客户端不应该依赖那些不需要的接口。
方法实现:
在抽象类中,我们可以对一些抽象方法做出基础实现;
而在接口类中,任何方法都只是一种规范,具体的功能需要子类实现
总结:
- 抽象类 : 规范
- 一般情况下 单继承 能实现的功能都是一样的,所以在父类中可以有一些简单的基础实现
- 多继承的情况 由于功能比较复杂,所以不容易抽象出相同的功能的具体实现写在父类中
- 抽象类还是接口类 : 面向对象的开发规范 所有的接口类和抽象类都不能实例化
- java :
- java里的所有类的继承都是单继承,所以抽象类完美的解决了单继承需求中的规范问题
- 但对于多继承的需求,由于java本身语法的不支持,所以创建了接口Interface这个概念来解决多继承的规范问题
- python
- python中没有接口类 :
- python中自带多继承 所以我们直接用class来实现了接口类
- python中支持抽象类 : 一般情况下 单继承 不能实例化
- 且可以实现python代码
继承的作用:
减少代码的重用 提高代码可读性 规范编程模式
几个名词:
抽象:抽象即抽取类似或者说比较像的部分。是一个从具题到抽象的过程。 继承:子类继承了父类的方法和属性 派生:子类在父类方法和属性的基础上产生了新的方法和属性
抽象类与接口类
1.多继承问题 在继承抽象类的过程中,我们应该尽量避免多继承; 而在继承接口的时候,我们反而鼓励你来多继承接口 2.方法的实现 在抽象类中,我们可以对一些抽象方法做出基础实现; 而在接口类中,任何方法都只是一种规范,具体的功能需要子类实现
钻石继承
新式类:广度优先 经典类:深度优先
3、接口类的多继承
from abc import abstractmethod,ABCMeta class Swim_Animal(metaclass=ABCMeta): #游泳的接口类 @abstractmethod def swim(self):print("游泳") class Walk_Animal(metaclass=ABCMeta): #走的接口类 @abstractmethod def walk(self):print("行走") class Fly_Animal(metaclass=ABCMeta): #飞的接口类 @abstractmethod def fly(self):print("飞") class Swan(Walk_Animal,Fly_Animal): #天鹅 def walk(self):pass def fly(self):pass class Oldying(Fly_Animal,Walk_Animal): #老鹰 def fly(self):pass def walk(self):pass class Tiger(Walk_Animal,Swim_Animal): #老虎 def walk(self):pass def swim(self):pass tiger1 = Tiger() oldying1 = Oldying() swan1 = Swan() #接口隔离原则:使用多个专门接口,而不是使用单一的总接口,即客户端不应该依赖哪些不需要的接口(如上免得代码:有走功能的动物单独继承走的类,有飞功能的类单独继承飞的类,每个功能单独写一个类,而不是所有功能都在一个类里)
多态
多态是指一个事物有多种形态,动物包含很多种(猪、狗、猫...,同时动物又包含很多同样的功能,如:吃东西、睡觉、走路...)
多态:
python本身就是一种多态语言,在别的语言里往类中传参时需要传数据类型,在python中不需要传数据类型,传类时也不要传父类的。
python:是一门动态强类型的语言
鸭子类型:
不依赖父类的情况下实现两个相似的类中的同名方法,(在其他语言里是依靠父类来约束子类的方法,在python里没有父类的事,所有类可以实现自己的方法)
接口类和抽象类:再python当中应用点并不是非常必要
多态性:
一 什么是多态动态绑定(在继承的背景下使用时,有时也称为多态性)
多态性是指在不考虑实例类型的情况下使用实例
在面向对象方法中一般是这样表述多态性: 向不同的对象发送同一条消息(!!!obj.func():是调用了obj的方法func,又称为向obj发送了一条消息func),不同的对象在接收时会产生不同的行为(即方法)。 也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。 比如:老师.下课铃响了(),学生.下课铃响了(),老师执行的是下班操作,学生执行的是放学操作,虽然二者消息一样,但是执行的效果不同
封装
广义上面向对象的封装:面向对象的思想本身就是一种封装(实现代码的保护),只让自己的对象调用自己类中的方法
狭义上的封装(面向对象的三大特性之一):
属性和方法都藏起来,不让你看见
【封装】
隐藏对象的属性和实现细节,仅对外提供公共访问方式。
【好处】
1. 将变化隔离;
2. 便于使用;
3. 提高复用性;
4. 提高安全性;
【封装原则】
1. 将不需要对外提供的内容都隐藏起来;
2. 把属性都隐藏,提供公共方法对其访问。
【私有变量和私有方法】
在python中用双下划线开头的方式将属性隐藏起来(设置成私有的)
1、一段封装的代码
其实这仅仅是一种变形操作,类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式: class Person: __key = 123 #类的属性应该是共享的,但是在key的前面加上__key,就会变成:_Person__key def __init__(self,name,passwd): self.name = name self.__passwd = passwd #前面加__方法后,这个属性就是一个私有属性,即不可以再类的外部调用了,最终变形为:_Person__passwd def get_pwd(self): return self.__passwd #只要在类的内部使用私有属性,就会自动带上_类名 def login(self): #正常的方法调用私有方法 self.__get_pwd() ouyang = Person("oyang",'abc123') #实例化 #print(ouyang.passwd) #加上了__方法的属性就不可以在外部备查看到了,会报错 print(ouyang._Person__passwd) #如果非要查看的话,可以通过这种方式查看,_类名__属性名,结果:abc123 print(ouyang.__dict__) #或者以字典的方式查看,结果:{'_Person__passwd': 'abc123', 'name': 'oyang'} ouyang.__high = 180 #在外部增加私有属性 print(ouyang.__high) #再外部增加的私有属性可以再外部调用结果为:{'__high': 180, '_Person__passwd': 'abc123', 'name': 'oyang'} print(ouyang.get_pwd()) #结果为:abc123
这种自动变形的特点:
1.类中定义的__x只能在内部使用,如self.__x,引用的就是变形的结果。
2.这种变形其实正是针对外部的变形,在外部是无法通过__x这个名字访问到的。
3.在子类定义的__x不会覆盖在父类定义的__x,因为子类中变形成了:_子类名__x,而父类中变形成了:_父类名__x,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的。
这种变形需要注意的问题是:
1.这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如a._A__N
2.变形的过程只在类的内部生效,在定义后的赋值操作,不会变形
1.1、封装与扩展性
封装在于明确区分内外,使得类实现者可以修改封装内的东西而不影响外部调用者的代码;而外部使用用者只知道一个接口(函数),只要接口(函数)名、参数不变,使用者的代码永远无需改变。这就提供一个良好的合作基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑。
#类的设计者 class Room: def __init__(self,name,owner,width,length,high): self.name=name self.owner=owner self.__width=width self.__length=length self.__high=high def tell_area(self): #对外提供的接口,隐藏了内部的实现细节,此时我们想求的是面积 return self.__width * self.__length #使用者 >>> r1=Room('卧室','egon',20,20,20) >>> r1.tell_area() #使用者调用接口tell_area #类的设计者,轻松的扩展了功能,而类的使用者完全不需要改变自己的代码 class Room: def __init__(self,name,owner,width,length,high): self.name=name self.owner=owner self.__width=width self.__length=length self.__high=high def tell_area(self): #对外提供的接口,隐藏内部实现,此时我们想求的是体积,内部逻辑变了,只需求修该下列一行就可以很简答的实现,而且外部调用感知不到,仍然使用该方法,但是功能已经变了 return self.__width * self.__length * self.__high #对于仍然在使用tell_area接口的人来说,根本无需改动自己的代码,就可以用上新功能 >>> r1.tell_area()
1.2、父类的私有属性可以被子类调用吗?
不可以!!
在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的.
class Foo: def __output_1(self): print("输出") class Zi(Foo): print(Foo.__output_1()) #不可以
总结:
所有的私有:都是在变量的左边加上双下划线
#对象的私有属性
#类中的私有方法
#类中的静态私有属性
#所有的私有属性都不能在类的外部使用
会用到私有的这个概念的场景
1、隐藏起一个属性,不想让类的外部调用
2、我想保护这个属性,不想让属性随意改变
3、我想保护这个属性,不被子类继承
总结:
继承:
单继承:
先抽象再继承,先有几个类之间的相同代码抽象出来,成为父类
子类没有自己的名字,就可以使用父类的方法和属性
如果子类自己有,一定先用自己的名字
在类中使用self的时候,一定要看清楚self指向谁
多继承:
新式类和经典类:
多类继承寻找名字的顺序:新式类广度优先,经典类深度优先
新式类中有一个.mro方法,查看广度优先的继承类顺序
在python3中:有一个super方法,根据广度优先的继承顺序查找上一个类
接口类 抽象类
python中没有接口类,有抽象类,通过abc模块中的metaclass = ABCMeta, @abstructmethod实现的
抽象类和接口类的本质是作代码规范用的,希望在子类中实现和父类方法名字完全一样的方法
在java的角度来看 是有区别的
java本来就支持单继承,所以就有抽象类
java没有多继承,所以为了接口隔离原则,设计了接口这个概念,支持多继承
python即支持单继承也支持多继承,所以对于接口类和抽象类的区别就不那么明显了
甚至python没有内置接口类
多态和鸭子类型:
多态 -- python天生支持多态,
鸭子类型: 不依赖父类的情况下实现两个相似的类中的同名方法
封装:
在python中的类里面,只要__一个名字,就把这个名字私有化了,
私有化了之后,就不能从类的外部直接调用了
静态属性 方法 对象属性 都可以私有化
这种私有化只是从代码级别做了变形,并没有真的约束
变形机制:__类型__名字(在类的外部用这个调用),在类的内部直接用__名字进行调用

浙公网安备 33010602011771号