1.面向过程与面向对象
1.1面向过程
1.1.1面向过程的定义
在说明之前先定义什么叫做面向过程
面向过程:核心是过程二字,过程指的是解决问题的步骤,设定一条流水线,机械式的思维。
或许各位没看明白,说白了就是我们想要做一件事,按照事物本身发展的过程,对过程一点点分割,分成一个个小块,或许整个事情很复杂也很麻烦,在我们分割后用很少的代码就可以完成整个过程,然后拼接在一起,整个流程就完成了。这就是面向过程,我们之前学习的各做作业就是按照面向过程去解决的。但是这个其中,我们试想,如果流程发生了变更,我们应该怎么办,难道要重新设定整个流程吗?
1.1.2面向过程的优缺点
先不说上面遗留的问题,先说下面向过程的优缺点
优点:复杂的问题流程化进而简单化。
缺点:可扩展性差
对于优点,我们在之前的作业完成中,应该深刻了解过,面对需求,分割分割,把问题解决就可以了
对于缺点,我们用以下这个人狗大战的例子来演示,
def peple(name,attack,life_value):#定义人这个函数,毕竟人不可能只有一个吗,男人和女人吗 data = { 'name': name, 'attack': attack, 'life_value': life_value, } return data def dog(name,attack,life_value):#定义狗这个函数,毕竟狗不肯能只能由一种,白狗黑狗以及花狗 data = { 'name': name, 'attack': attack, 'life_value': life_value, } return data def attack(p,d): """人打狗功能""" d['life_value'] -= p['attack'] #被打了,要掉血 print("人[%s] 打了 狗[%s]。。。,[%s]的生命值还有[%s]" % (p['name'], d['name'],d['name'],d['life_value'])) def bite(d,p): """狗咬人功能""" p['life_value'] -= d['attack'] print("狗[%s] 咬了 人[%s]。。。,[%s]的生命值还有[%s]" % (d['name'], p['name'],p['name'],p['life_value']))
上面我们定义了人和狗这俩个函数,以及人打狗,狗咬人俩个函数,我们要生成人员以及互相攻击
alex = people("Alex",100,1000) black_girl = people("Black girl",80,700) d = dog("白狗",200,800) attack(alex,d) bite(d,black_girl) #结果是 人[Alex] 打了 狗[白狗]。。。,[白狗]的生命值还有[700] 狗[白狗] 咬了 人[Black girl]。。。,[Black girl]的生命值还有[500]
看着这些是不是说我完成了,定义了人和狗,还有互相攻击,互相怎么咬都可以完成了哈,但是如果我们在攻击的时候把人当做狗来传进去不就成人咬人或者是人咬狗了,对于咬这个事情只能是狗才能有动作,对于人应该就是用工具吗,可以说这个改简单啊,把狗咬人打都定义在自己的特性中
def person(name,attack_val,life_value): def attack( d): """人打狗功能""" d['life_value'] -= attack_val # 被打了,要掉血 print("人[%s] 打了 狗[%s]。。。,[%s]的生命值还有[%s]" % (name, d['name'], d['name'], d['life_value'])) data = { 'name':name, 'attack_val':attack_val, 'life_value':life_value, 'attack':attack } return data def dog(name, attack_val, life_value): def bite(p): """狗咬人功能""" p['life_value'] -= attack_val print("狗[%s] 咬了 人[%s]。。。,[%s]的生命值还有[%s]" % (name, p['name'], p['name'], p['life_value'])) data = { 'name': name, 'attack_val': attack_val, 'life_value': life_value, 'bite':bite } return data
可如果是还有更多其他的需求呢?这样不是变化太大了吗,这就是上面我们一直在说的面向过程编程的痛点就是可扩展性差。但是上面这些问题都可以在面向对象中解决。
1.2面向对象
1.2.1面向对象的定义
先说一下,什么叫做面向对象
面向对象:核心就是对象二字,对象的特征就是特征与技能的结合体
好像说了这么一句啥也没说
1.2.2面向对象的优缺点
优点:可扩展性强
缺点:编程复杂度高
应用环境:用户需求经常变化,像一般互联网应用,游戏以及企业内部应用
面向过程==个人视角
我要吃面包,如果没有,那么我们就是拿钱穿衣服选择自己喜欢的商店,买喜欢的面包,然后去吃就好了
面向对象==上帝视角
我要创造一个世界,我一个人去创建好累啊,我创造人,我创造动物,让他们自己去做好了,我看着就行
说了这么多感觉还是没讲什么对于面向过程的好处啊,下面一点点的讲
2.类的定义与基本性质
2.1名词解释
类:一个类即是对一类拥有相同属性的对象的抽象、蓝图、原型、模板。在类中定义了这些对象的都具备的属性(variables(data))、共同的方法,简单的说就是一系列对象相似的特征与技能的结合体。(强调:站在不同的角度,得到的分类也是不一致的)
属性:人类包含很多特征,把这些特征用程序来描述的话,叫做属性,比如年龄、身高、性别、姓名等都叫做属性,一个类中,可以有多个属性
方法:人类不止有身高、年龄、性别这些属性,还能做好多事情,比如说话、走路、吃饭等,相比较于属性是名词,说话、走路是动词,这些动词用程序来描述就叫做方法。
实例(对象):一个对象即是一个类的实例化后实例,一个类必须经过实例化后方可在程序中调用,一个类可以实例化多个对象,每个对象亦可以有不同的属性,就像人类是指所有人,每个人是指具体的对象,人与人之前有共性,亦有不同
实例化:把一个类转变为一个对象的过程就叫实例化
在现实中,先有对象,才会有类,而在程序中,必须先定义类,才会调用类来产生对象
2.2类的基本性质
类的定义,就拿我们之前在面向过程中人狗大战这个例子来看
#定义狗 class dog: enercise = '四条腿跑' def bite(self): pass #定义人 class people: enumerate = '直立行走' def attack(self): pass
我们定义了狗的特性,四条腿跑,会咬人这个基本特征,人是直立行走,会打这个特征。
我们会发现,类和函数最大的区别就是类在定义的时候就开始就开始运行,而不是函数只有在调用的时候才会执行
查看类的名称空间
print(dog.__dict__) #执行结果 {'__doc__': None, '__module__': '__main__', 'enercise': '四条腿跑', '__dict__': <attribute '__dict__' of 'dog' objects>, 'bite': <function dog.bite at 0x00000233E9592378>, '__weakref__': <attribute '__weakref__' of 'dog' objects>}
类内部定义的变量称之为类的数据属性:例如狗的enercise = '四条腿跑'
类内部定义的函数称之为类的函数属性:例如狗的attack函数
类的实例化,就是类就在类后面加上()就可以实例化一个类,例如分别要实例化一个狗一个人
d1 = dog()
p1= people()
这样就生成一个狗一个人,可是这样生成狗和人都只有共同属性,可是对于每一只狗每个人都是有自身特殊的属性,这个时候就需要一个特殊的函数__init__函数,为对象定制自己独有的特性
#定义狗 class dog: enercise = '四条腿跑' def bite(self): pass def __init__(self,color,attack,life_value): self.color = color self.attack = attack self.life_value = life_value d1 = dog('white',80,500)
这样我们就定义了一个白色毛色的80攻击力,500生命值得这一独立的狗,相比之前他有自己独立的毛色攻击力生命值,同样我们也可以定义独特的人
对于__init__函数,我们在创建或者是实例化的时候就会自动运行
上面我们实例化一个对象会了,现在学习一下类的增删改查
#查属性,查狗的毛色 print(d1.color) #改属性,狗摔了一跤,攻击力降低 d1.attack = 50 #删属性, del d1.color #增加属性,狗分公母 d1.sex = 'male' #这样我们就对d1这个实例化的对象做了增删改查
这个地方我们强调一下,
对象:特征与技能的结合
类:类是一系列的对象相似技能的特征与相似的技能的结合体
类中的数据属性:是所有对象共有的,指向的都是一样的内存地址
类中的函数属性:是绑定对象,绑定不同的对象就有不同的绑定方法,对象调用绑定方式时,会把对象本身当做第一个参数,传给self
类的函数是给对象来使用的,谁来谁调用
如果对象访问自己的属性时,会先从自己的名称空间访问,没有从所属的类中查找,如果没有则不会从全局变量中找,只会在类的名称空间中查找
如果你理解了上面的部分,让我们愉快的改写上面的人狗大战吧。
#定义狗 class dog: enercise = '四条腿跑' def __init__(self,color,attack,life_value): self.color = color self.attack = attack self.life_value = life_value def bite(self,menory): menory.life_value -= self.attack #人被咬了要掉血 print("狗咬了人[%s]。。。,[%s]的生命值还有[%s]" % (menory.name,menory.name,menory.life_value)) #定义人 class people: enumerate = '直立行走' def __init__(self,name,sex,attack,life_value): self.name = name self.sex = sex self.attack = attack self.life_value = life_value def attacking(self, menory): menory.life_value -= self.attack # 狗被打了,要掉血 print("人[%s] 打了 狗。。。,狗的生命值还有[%s]" % (self.name,menory.life_value)) p1 = people('张三','男',100,300) p2 = people('美女','女',50,200) d1 = dog('white',80,500) p1.attacking(d1) d1.bite(p2)
这样狗类和人类就已经定义好了,就可以生成不同的狗以及不同的人,放肆的人狗大战吧,而且咬这个功能只有狗有而人没有。以后有了新的功能的话,直接在类中新增新的方法就好了。
补充说明:
- 站的角度不同,定义出的类是截然不同的
- 现实中的类并不完全等于程序的类
- 为了编程需要,程序也有可能会使用现实中不存在的类
python中一切都为对象,在python3中统一了类与类型的概念
在python3中类型就是类,例如list以及dic
list.append(L,4) 相当于调用list的这个类内部的append这个函数对L进行添加元素。
3.类的三大特性
3.1继承
3.1.1继承的基本定义
继承指的是类与类之间的关系,是一种什么“是”什么的关系,继承的功能之一就是用来解决代码重用问题,继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类
父类:称之为基类或者是超类
子类:派生类或者是子类
python中类的继承可以分为单继承和多继承,具体演示如下
class ParentClass1: #定义父类1 pass class ParentClass2: #定义父类2 pass class SubClass1(ParentClass1): #单继承,基类是ParentClass1,派生类是SubClass pass class SubClass2(ParentClass1,ParentClass2): #python支持多继承,用逗号分隔开多个继承的类 pass
可以看出SubClass1就是单继承,只是继承上面一个父类,而SubClass就继承上面的父类1以及父类2,
对于一个子类我们有时看不出他继承了哪些父类也可以用以下办法来查询
>>> SubClass1.__bases__ #__base__只查看从左到右继承的第一个子类 (<class '__main__.ParentClass1'>,) >>> SubClass2.__bases__#,__bases__则是查看所有继承的父类 (<class '__main__.ParentClass1'>, <class'__main__.ParentClass2'>)
对于我们之前讲的人狗大战同样适用,例如人和狗分别属于人类和狗类,而人类和狗类都可以属于动物类,所以人类和狗类都属于动物类,是动物类的子类
3.1.2派生
在将派生定义之前,我们说了人类和狗类都属于动物类,那人类和狗类就有了共同的特点,胎生啊什么的,除此之外,人类和狗类是有着明显的不同,例如一个是直立行走而另外一个是四条腿跑,那这样人类和狗类在继承了动物的这个类之后都有着自身不同的特点。这就需要到了派生,下面用英雄联盟英雄对战做例子
当然子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了。
class Riven(Hero): camp='Noxus' def attack(self,enemy): #在自己这里定义新的attack,不再使用父类的attack,且不会影响父类 print('from riven') def fly(self): #在自己这里定义新的 print('%s is flying' %self.nickname)
在子类中,新建的重名的函数属性,在编辑函数内功能的时候,有可能需要重用父类中重名的那个函数功能,应该是用调用普通函数的方式,即:类名.func(),此时就与调用普通函数无异了,因此即便是self参数也要为其传值
class Riven(Hero): camp='Noxus' def __init__(self,nickname,aggressivity,life_value,skin): Hero.__init__(self,nickname,aggressivity,life_value) #调用父类功能 self.skin=skin #新属性 def attack(self,enemy): #在自己这里定义新的attack,不再使用父类的attack,且不会影响父类 Hero.attack(self,enemy) #调用功能 print('from riven') def fly(self): #在自己这里定义新的 print('%s is flying' %self.nickname) r1=Riven('锐雯雯',57,200,'比基尼') r1.fly() print(r1.skin) ''' 运行结果 锐雯雯 is flying 比基尼 '''
3.1.3继承的原理以及经典类新式类
python到底是如何实现继承的,对于你定义的每一个类,python会计算出一个方法解析顺序(MRO)列表,这个MRO列表就是一个简单的所有基类的线性顺序列表,例如
>>> F.mro() #等同于F.__mro__ [<class '__main__.F'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
为了实现继承,python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:
- 子类会先于父类检查
- 多个父类会根据他们在列表中顺序被检查
- 如果对下一个类存在俩个合法选择,选择第一个父类
在讲述下面其他问题的时候,需要一点其他知识,经典类与新式类的区别
在python2.7中有经典类与新式类的区别
经典类:没有继承object的类,以及它的子类
新式类:继承object的类,以及它的子类
(备注:object类是python自带的类,包含各种自带的各种用法,是一个基类)
在python3中,所有的类都是默认继承,都为新式类
>>> ParentClass1.__bases__ (<class 'object'>,) >>> ParentClass2.__bases__ (<class 'object'>,)
现在我们继续说继承原理
Java和C#中子类只能继承一个父类,而Python中子类可以同时继承多个父类,如果继承了多个父类,那么属性的查找方式有两种,分别是:深度优先和广度优先
这个时候就可以说明经典类与新式类的区别,经典类是深度优先,一条道走到黑,而新式类是广度优先
可以看以下代码
class A(object): def test(self): print('from A') class B(A): def test(self): print('from B') class C(A): def test(self): print('from C') class D(B): def test(self): print('from D') class E(C): def test(self): print('from E') class F(D,E): # def test(self): # print('from F') pass f1=F() f1.test() print(F.__mro__) #只有新式才有这个属性可以查看线性列表,经典类没有这个属性 #新式类继承顺序:F->D->B->E->C->A #经典类继承顺序:F->D->B->A->E->C #python3中统一都是新式类 #pyhon2中才分新式类与经典类
3.1.4子类重用父类属性
在子类派生出的新方法中,往往需要重用父类的方法,我们有两种方式实现
方式一:指名道姓,即父类名.父类方法(),代码如下
class Vehicle: #定义交通工具类 Country='China' def __init__(self,name,speed,load,power): self.name=name self.speed=speed self.load=load self.power=power def run(self): print('开动啦...') class Subway(Vehicle): #地铁 def __init__(self,name,speed,load,power,line): Vehicle.__init__(self,name,speed,load,power) self.line=line def run(self): print('地铁%s号线欢迎您' %self.line) Vehicle.run(self) line13=Subway('中国地铁','180m/s','1000人/箱','电',13) line13.run()
定义了一个父类交通工具,子类地铁继承了交通工具的方法,在对自己的定义的时候就可以直接可以调用
方式二:super(),代码如下
class Vehicle: #定义交通工具类 Country='China' def __init__(self,name,speed,load,power): self.name=name self.speed=speed self.load=load self.power=power def run(self): print('开动啦...') class Subway(Vehicle): #地铁 def __init__(self,name,speed,load,power,line): #super(Subway,self) 就相当于实例本身 在python3中super()等同于super(Subway,self) super().__init__(name,speed,load,power) self.line=line def run(self): print('地铁%s号线欢迎您' %self.line) super(Subway,self).run() class Mobike(Vehicle):#摩拜单车 pass line13=Subway('中国地铁','180m/s','1000人/箱','电',13) line13.run()
这两种方式的区别是:方式一是跟继承没有关系的,而方式二的super()是依赖于继承的,并且即使没有直接继承关系,super仍然会按照mro继续往后查找
对于第二种方法还有一个小练习,可以看以下代码,为什么是这样
class A: def test(self):
print('from A') super().test() class B: def test(self): print('from B') class C(A,B): pass c=C() c.test() #打印结果:from A
from B print(C.mro()) #[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
对于c我们会先从类本身去查找,c找不到,就会从他的父类去找,按照顺序在类A查找,找到后打印,但是函数后面还有一个super,而这个super又会按照c的mro表继续去找B,这样又把B打印了,结果就是打印了俩个,并不是按照我们所想只打印A。
3.1.5组合
软件重用的重要方式除了继承之外还有另外一种方式,即:组合
组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合
废话不多说,我们就按照一个选课系统的例子来让大家明白,代码如下
class people:#定义了人类这个父类 school = 'luffycity' def __init__(self,name,sex,age): self.name = name self.sex = sex self.age = age class teacher(people):#定义教师 def __init__(self,name,sex,age,leval,salary): super().__init__(name,sex,age) self.leval = leval self.salary = salary def teaching(self): print("%s is teaching"%self) class student(people):#定义学生 def __init__(self,name,sex,age,class_time): super().__init__(name,sex,age) self.class_time = class_time def learning(self): print("%s is learning"%self) stu1 = student('gao-bq','男',18,'08:30')#实例化一个学生 alex = teacher('alex','男',16,5,10)#实例化一个教师
这样我们就定义一个父类以及俩个子类,同时实例化了一个学生以及教师,既然是一个选课系统,那么就有课程,如是以前我们就会想着既然是课程,那么我在学生函数里面定义一个课程,那么我们在实例化的时候就可以有课程属性,那么如果学生不止学一门还有其他课程,难道在定义吗?这样就很浪费时间了,那么我们就要定义一个课程的类,生成几个课程的对象,方便我们调用
class couse:#定义课程类 def __init__(self,couse_name,couse_price,couse_time): self.couse_name = couse_name self.couse_price = couse_price self.couse_time = couse_time def tell_info(self): print("课程名称<%s> 课程价格<%s> 课程学习时间<%s>"%(self.couse_name,self.couse_price,self.couse_time)) python = couse('python','10000','6mons')#实例化python这个课程 lunix = couse ('linux','8000','5mons')#实例化linux这个课程 stu1.couse = python stu1.couse.tell_info()
输出结果
课程名称<python> 课程价格<10000> 课程学习时间<6mons>
我们发现对于课程这个对象我们并不是继承的同样可以使用它的方法,那么这就是组合。
对于组合和继承之间的区别,其实只要记住以下
继承就是什么是什么的关系,例如学生是属于人类的,是一种是否的关系
组合就是什么有什么关系,例如学生有课程的这个对象,而这个对象并不是继承过来的,只是从其他地方拿过来用的
当类之间有显著不同,并且较小的类是较大的类所需要的组件时,用组合比较好
3.1.6抽象类与归一性
hi boy,给我开个查询接口。。。此时的接口指的是:自己提供给使用者来调用自己功能的方式\方法\入口,
接口提取了一群类共同的函数,可以把接口当做一个函数的集合。
然后让子类去实现接口中的函数。
这么做的意义在于归一化,什么叫归一化,就是只要是基于同一个接口实现的类,那么所有的这些类产生的对象在使用时,从用法上来说都一样。
归一化的好处在于:
- 归一化让使用者无需关心对象的类是什么,只需要的知道这些对象都具备某些功能就可以了,这极大地降低了使用者的使用难度。
- 归一化使得高层的外部使用者可以不加区分的处理所有接口兼容的对象集合
- 就好象linux的泛文件概念一样,所有东西都可以当文件处理,不必关心它是内存、磁盘、网络还是屏幕(当然,对底层设计者,当然也可以区分出“字符设备”和“块设备”,然后做出针对性的设计:细致到什么程度,视需求而定)。
- 再比如:我们有一个汽车接口,里面定义了汽车所有的功能,然后由本田汽车的类,奥迪汽车的类,大众汽车的类,他们都实现了汽车接口,这样就好办了,大家只需要学会了怎么开汽车,那么无论是本田,还是奥迪,还是大众我们都会开了,开的时候根本无需关心我开的是哪一类车,操作手法(函数调用)都一样
可以使用继承,其实继承有两种用途
一:继承基类的方法,并且做出自己的改变或者扩展(代码重用):实践中,继承的这种用途意义并不很大,甚至常常是有害的。因为它使得子类与基类出现强耦合。
二:声明某个子类兼容于某基类,定义一个接口类(模仿java的Interface),接口类中定义了一些接口名(就是函数名)且并未实现接口的功能,子类继承接口类,并且实现接口中的功能
看以下代码
class Interface:#定义接口Interface类来模仿接口的概念,python中压根就没有interface关键字来定义一个接口。 def read(self): #定接口函数read pass def write(self): #定义接口函数write pass class Txt(Interface): #文本,具体实现read和write def read(self): print('文本数据的读取方法') def write(self): print('文本数据的读取方法') class Sata(Interface): #磁盘,具体实现read和write def read(self): print('硬盘数据的读取方法') def write(self): print('硬盘数据的读取方法') class Process(Interface): def read(self): print('进程数据的读取方法') def write(self): print('进程数据的读取方法')
看上方代码,我们在父类定义了读写的操作,下方硬盘,文件夹以及文本文档都调用了读写的功能,可是如果就是不用,非得改为自己喜欢的也是可以的,并没有一个强制性的工作让下方的子类必须使用这些方法,这就需要用到抽象类
抽象类:与java一样,python也有抽象类的概念但是同样需要借助模块实现,抽象类是一个特殊的类,它的特殊之处在于只能被继承,不能被实例化
如果说类是从一堆对象中抽取相同的内容而来的,那么抽象类就是从一堆类中抽取相同的内容而来的,内容包括数据属性和函数属性。
比如我们有香蕉的类,有苹果的类,有桃子的类,从这些类抽取相同的内容就是水果这个抽象的类,你吃水果时,要么是吃一个具体的香蕉,要么是吃一个具体的桃子。。。。。。你永远无法吃到一个叫做水果的东西。
从设计角度去看,如果类是从现实对象抽象而来的,那么抽象类就是基于类抽象而来的。
从实现角度来看,抽象类与普通类的不同之处在于:抽象类中只能有抽象方法(没有实现功能),该类不能被实例化,只能被继承,且子类必须实现抽象方法。这一点与接口有点类似,但其实是不同的,看以下代码你就可以顺利的明白了
#一切皆文件 import abc #利用abc模块实现抽象类 class All_file(metaclass=abc.ABCMeta): all_type='file' @abc.abstractmethod #定义抽象方法,无需实现功能 def read(self): '子类必须定义读功能' pass @abc.abstractmethod #定义抽象方法,无需实现功能 def write(self): '子类必须定义写功能' pass # class Txt(All_file): # pass # # t1=Txt() #报错,子类没有定义抽象方法 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),而接口只强调函数属性的相似性。
抽象类是一个介于类和接口直接的一个概念,同时具备类和接口的部分特性,可以用来实现归一化设计
3.2多态
3.2.1多态的定义
多态就是指的一类事物的多种形态
动物有多种形态:人,狗,猪,文本就有可执行文件,文本文件
多态性是指在不考虑实例类型的情况下使用实例,多态性分为静态多态性和动态多态性
静态多态性:如任何类型都可以用运算符+进行运算
动态多态性:如下
peo=People() dog=Dog() pig=Pig() #peo、dog、pig都是动物,只要是动物肯定有talk方法 #于是我们可以不用考虑它们三者的具体是什么类型,而直接使用 peo.talk() dog.talk() pig.talk() #更进一步,我们可以定义一个统一的接口来使用 def func(obj): obj.talk()
还有一个例子大家会明白的更清楚一点,你学开车,只是拿一种车去学习,学成之后不可能只会开教练车啊,对于不同的车他就是属于车这一类啊,其他车你也会开啊
3.2.2多态性的使用
其实大家从上面多态性的例子可以看出,我们并没有增加什么新的知识,也就是说python本身就是支持多态性的,这么做的好处是什么呢?
1.增加了程序的灵活性
以不变应万变,不论对象千变万化,使用者都是同一种形式去调用,如func(animal)
2.增加了程序额可扩展性
通过继承animal类创建了一个新的类,使用者无需更改自己的代码,还是用func(animal)去调用
>>> class Cat(Animal): #属于动物的另外一种形态:猫 ... def talk(self): ... print('say miao') ... >>> def func(animal): #对于使用者来说,自己的代码根本无需改动 ... animal.talk() ... >>> cat1=Cat() #实例出一只猫 >>> func(cat1) #甚至连调用方式也无需改变,就能调用猫的talk功能 say miao ''' 这样我们新增了一个形态Cat,由Cat类产生的实例cat1,使用者可以在完全不需要修改自己代码的情况下。使用和人、狗、猪一样的方式调用cat1的talk方法,即func(cat1)
3.2.3鸭子类型
Python崇尚鸭子类型,即‘如果看起来像、叫声像而且走起路来像鸭子,那么它就是鸭子’
python程序员通常根据这种行为来编写程序。例如,如果想编写现有对象的自定义版本,可以继承该对象,也可以创建一个外观和行为像,但与它无任何关系的全新对象,后者通常用于保存程序组件的松耦合度。
例1:利用标准库中定义的各种‘与文件类似’的对象,尽管这些对象的工作方式像文件,但他们没有继承内置文件对象的方法
#二者都像鸭子,二者看起来都像文件,因而就可以当文件一样去用 class TxtFile: def read(self): pass def write(self): pass class DiskFile: def read(self): pass def write(self): pass
例2:序列类型有多种形态:字符串,列表,元组,但他们直接没有直接的继承关系
#str,list,tuple都是序列类型 s=str('hello') l=list([1,2,3]) t=tuple((4,5,6)) #我们可以在不考虑三者类型的前提下使用s,l,t s.__len__() l.__len__() t.__len__() len(s) len(l) len(t)
3.3封装
从封装本身的意思去理解,封装就好像是拿来一个麻袋,把小猫,小狗,小王八,还有alex一起装进麻袋,然后把麻袋封上口子。照这种逻辑看,封装=‘隐藏’,这种理解是相当片面的
3.3.1隐藏
在python中用双下划线开头的方式将属性隐藏起来(设置成私有的)
#其实这仅仅这是一种变形操作 #类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式: class A: __N=0 #类的数据属性就应该是共享的,但是语法上是可以把类的数据属性设置成私有的如__N,会变形为_A__N def __init__(self): self.__X=10 #变形为self._A__X def __foo(self): #变形为_A__foo print('from A') def bar(self): self.__foo() #只有在类内部才可以通过__foo的形式访问到. #A._A__N是可以访问到的,即这种操作并不是严格意义上的限制外部访问,仅仅只是一种语法意义上的变形
这种自动变形的特点:
- 类中定义的__x只能在内部使用,如self.__x,引用的就是变形的结果。
- 这种变形其实正是针对外部的变形,在外部是无法通过__x这个名字访问到的。
- 在子类定义的__x不会覆盖在父类定义的__x,因为子类中变形成了:_子类名__x,而父类中变形成了:_父类名__x,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的。
这种变形需要注意的问题是:
- 这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如a._A__N
- 变形的过程只在类的定义是发生一次,在定义后的赋值操作,不会变形
- 在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的
#正常情况 >>> class A: ... def fa(self): ... print('from A') ... def test(self): ... self.fa() ... >>> class B(A): ... def fa(self): ... print('from B') ... >>> b=B() >>> b.test() from B #把fa定义成私有的,即__fa >>> class A: ... def __fa(self): #在定义时就变形为_A__fa ... print('from A') ... def test(self): ... self.__fa() #只会与自己所在的类为准,即调用_A__fa ... >>> class B(A): ... def __fa(self): ... print('from B') ... >>> b=B() >>> b.test() from A
3.3.2封装
封装不是单纯意思的隐藏,封装主要有俩种形式
1:封装数据
将数据隐藏起来这不是目的。隐藏起来然后对外提供操作该数据的接口,然后我们可以在接口附加上对该数据操作的限制,以此完成对数据属性操作的严格控制。
class Teacher: def __init__(self,name,age): self.__name=name self.__age=age def tell_info(self): print('姓名:%s,年龄:%s' %(self.__name,self.__age)) def set_info(self,name,age): if not isinstance(name,str): raise TypeError('姓名必须是字符串类型') if not isinstance(age,int): raise TypeError('年龄必须是整型') self.__name=name self.__age=age t=Teacher('egon',18) t.tell_info() t.set_info('egon',19) t.tell_info()
2:封装方法:目的是隔离复杂度
#取款是功能,而这个功能有很多功能组成:插卡、密码认证、输入金额、打印账单、取钱 #对使用者来说,只需要知道取款这个功能即可,其余功能我们都可以隐藏起来,很明显这么做 #隔离了复杂度,同时也提升了安全性 class ATM: def __card(self): print('插卡') def __auth(self): print('用户认证') def __input(self): print('输入取款金额') def __print_bill(self): print('打印账单') def __take_money(self): print('取款') def withdraw(self): self.__card() self.__auth() self.__input() self.__print_bill() self.__take_money() a=ATM() a.withdraw()
- 电视机本身是一个黑盒子,隐藏了所有细节,但是一定会对外提供了一堆按钮,这些按钮也正是接口的概念,所以说,封装并不是单纯意义的隐藏!!!
- 快门就是傻瓜相机为傻瓜们提供的方法,该方法将内部复杂的照相功能都隐藏起来了
提示:在编程语言里,对外提供的接口(接口可理解为了一个入口),可以是函数,称为接口函数,这与接口的概念还不一样,接口代表一组接口函数的集合体。
3.3.3property函数
property是一种特殊的属性,访问它时会执行一段功能(函数)然后返回值
例一:BMI指数(bmi是计算而来的,但很明显它听起来像是一个属性而非方法,如果我们将其做成一个属性,更便于理解)
成人的BMI数值:
过轻:低于18.5
正常:18.5-23.9
过重:24-27
肥胖:28-32
非常肥胖, 高于32
体质指数(BMI)=体重(kg)÷身高^2(m)
EX:70kg÷(1.75×1.75)=22.86
代码如下
class People: def __init__(self,name,weight,height): self.name=name self.weight=weight self.height=height @property def bmi(self): return self.weight / (self.height**2) p1=People('egon',75,1.85) print(p1.bmi)
例二:圆的周长和面积
import math class Circle: def __init__(self,radius): #圆的半径radius self.radius=radius @property def area(self): return math.pi * self.radius**2 #计算面积 @property def perimeter(self): return 2*math.pi*self.radius #计算周长 c=Circle(10) print(c.radius) print(c.area) #可以向访问数据属性一样去访问area,会触发一个函数的执行,动态计算出一个值 print(c.perimeter) #同上 ''' 输出结果: 314.1592653589793 62.83185307179586 '''
注意:此时的特性area和perimeter不能被赋值
c.area=3 #为特性area赋值 ''' 抛出异常: AttributeError: can't set attribute '''
将一个类的函数定义成特性以后,对象再去使用的时候obj.name,根本无法察觉自己的name是执行了一个函数然后计算出来的,这种特性的使用方式遵循了统一访问的原则
对于python如果非要改property函数的值也是可以的,按照以下方法
class Foo: def __init__(self,val): self.__NAME=val #将所有的数据属性都隐藏起来 @property def name(self): return self.__NAME #obj.name访问的是self.__NAME(这也是真实值的存放位置) @name.setter def name(self,value): if not isinstance(value,str): #在设定值之前进行类型检查 raise TypeError('%s must be str' %value) self.__NAME=value #通过类型检查后,将值value存放到真实的位置self.__NAME @name.deleter def name(self): raise TypeError('Can not delete') f=Foo('egon') print(f.name) # f.name=10 #抛出异常'TypeError: 10 must be str' del f.name #抛出异常'TypeError: Can not delete'
封装的扩展性
封装在于明确区分内外,使得类实现者可以修改封装内的东西而不影响外部调用者的代码;而外部使用用者只知道一个接口(函数),只要接口(函数)名、参数不变,使用者的代码永远无需改变。这就提供一个良好的合作基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑。
代码如下
#类的设计者 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()
对于使用者来说他只是调用这个接口,在接口没有大范围变动的时候,对于调用者来说都是无感的,不影响他的使用,这样的话对于程序的扩展性就有了很好的扩展。
class People:
def __init__(self,name,weight,height):
self.name=name
self.weight=weight
self.height=height
@property
def bmi(self):
return self.weight / (self.height**2)
p1=People('egon',75,1.85)
print(p1.bmi)