面向对象程序设计
一 面向对象的程序设计和面向函数式的程序设计
函数式编程:精简,不修改状态
优点是:极大的降低了程序的复杂度
缺点是:一套流水线或者流程就是用来解决一个问题
面向对象编程
优点是:解决了程序的扩展性。对某一个对象单独修改,会立刻反映到整个体系中。
缺点:可控性差,无法向面向过程的程序设计流水线式的可以很精准的预测问题的处理流程与结果,
面向对象的程序一旦开始就由对象之间的交互解决问题
面向对象的软件开发
1.面向对象分析(object oriented analysis ,OOA)
2 面向对象设计(object oriented design,OOD)
3 面向对象编程(object oriented programming,OOP)
4 面向对象测试(object oriented test,OOT)
5 面向对象维护(object oriendted soft maintenance,OOSM)
二 类和对象
2.1 什么是对象,什么是类
python中一切皆为对象,且python3统一了类与类型的概念,类型就是类
基于面向对象设计一个款游戏:英雄联盟,每个玩家选一个英雄,每个英雄都有自己的特征和和技能,特征即数据属性,技能即方法属性,特征与技能的结合体就一个对象。
从一组对象中提取相似的部分就是类,类所有对象都具有的特征和技能的结合体。
在python中,用变量表示特征,用函数表示技能,因而类是变量与函数的结合体,对象是变量与方法(指向类的函数)的结合体。
大前提:
1.只有在python2中才分新式类和经典类,python3中统一都是新式类
2.新式类和经典类声明的最大不同在于,所有新式类必须继承至少一个父类
3.所有类甭管是否显式声明父类,都有一个默认继承object父类(讲继承时会讲,先记住)
在python2中的区分
经典类:
class 类名:
pass
新式类:
class 类名(父类):
pass
在python3中,上述两种定义方式全都是新式类
#在python2中,新式类 class B(object):pass #有继承 class C(B):pass print(B.__bases__)#查看基类 print(C.__bases__) #在python2中,经典类 class D:pass print(D.__bases__) # 在python3中,所有的类都是新式类 class A:pass print(A.__bases__)
2.2 初识类
2.2.1定义一个类
class Garen: #定义英雄盖伦的类,不同的玩家可以用它实例出自己英雄; camp='Demacia' #所有玩家的英雄(盖伦)的阵营都是Demacia; def __init__(self,nickname,aggressivity=58,life_value=455): #英雄的初始攻击力58...; self.nickname=nickname #为自己的盖伦起个别名; self.aggressivity=aggressivity #英雄都有自己的攻击力; self.life_value=life_value #英雄都有自己的生命值; def attack(self,enemy): #普通攻击技能,enemy是敌人; enemy.life_value-=self.aggressivity #根据自己的攻击力,攻击敌人就减掉敌人的生命
2.2 类有两种作用:属性引用和实例化
属性引用(类名.属性)
>>> Garen.camp #引用类的数据属性,该属性与所有对象/实例共享 'Demacia' >>> Garen.attack #引用类的函数属性,该属性也共享 <function Garen.attack at 0x101356510> >>> Garen.name='Garen' #增加数据属性 >>> del Garen.name #删除数据属性
实例化(__init__与self)
实例化:类名+括号
>>> g1=Garen('草丛伦') #就是在执行Garen.__init__(g1,'草丛伦'),然后执行__init__内的代码g1.nickname=‘草丛伦’等
一:我们定义的类的属性到底存到哪里了?有两种方式查看 dir(类名):查出的是一个名字列表 类名.__dict__:查出的是一个字典,key为属性名,value为属性值 二:特殊的类属性 类名.__name__# 类的名字(字符串) 类名.__doc__# 类的文档字符串 类名.__base__# 类的第一个父类(在讲继承时会讲) 类名.__bases__# 类所有父类构成的元组(在讲继承时会讲) 类名.__dict__# 类的字典属性 类名.__module__# 类定义所在的模块 类名.__class__# 实例对应的类(仅新式类中)
类名称空间与对象/实例名称空间
创建一个类就会创建一个类的名称空间,用来存储类中定义的所有名字,这些名字称为类的属性
而类有两种属性:数据属性和函数属性
其中类的数据属性是共享给所有对象的,内存地址相同
而类的函数属性是绑定到所有对象的,内存地址不同
创建一个对象/实例就会创建一个对象/实例的名称空间,存放对象/实例的名字,称为对象/实例的属性
在obj.name会先从obj自己的名称空间里找name,找不到则去类中找,类也找不到就找父类...最后都找不到就抛出异常
总结
类有两种作用:属性引用和实例化
1 属性引用(类名.属性)
引用类的数据属性
引用类的函数属性
2 实例化(__init__与self)
类名加括号就是实例化,会自动触发__init__函数的运行,可以用它来为每个实例定制自己的特征
实例化:类名+括号
对象/实例只有一种作用:属性引用
对象/实例本身只有数据属性,但是python的class机制会将类的函数绑定到对象上,称为对象的方法,或者叫绑定方法,绑定方法唯一绑
定一个对象,同一个类的方法绑定到不同的对象上,属于不同的方法,内存地址都不会一样
对象的绑定方法的特别之处在于:obj.func()会把obj传给func的第一个参数。
三 继承与派生
3.1 什么是继承
继承是一种创建新的类的方式,在python中,新建的类可以继承自一个或者多个父类,原始类称为基类或超类,新建的类称为派生类或子类。
继承:是基于抽象的结果,通过编程语言去实现它,肯定是先经历抽象这个过程,才能通过继承的方式去表达出抽象的结构。
python中类的继承分为:单继承和多继承
class ParentClass1: #定义父类 pass class ParentClass2: #定义父类 pass class SubClass1(ParentClass1): #单继承,基类是ParentClass1,派生类是SubClass pass class SubClass2(ParentClass1,ParentClass2): #python支持多继承,用逗号分隔开多个继承的类 pass
查看继承
print(ParentClass1.__bases__) #查看继承 print(SubClass1.__bases__) print(SubClass2.__bases__)
3.2 继承与重用性
在开发程序的过程中,如果我们定义了一个类A,然后又想新建立另外一个类B,但是类B的大部分内容与类A的相同时
我们不可能从头开始写一个类B,这就用到了类的继承的概念。
通过继承的方式新建类B,让B继承A,B会‘遗传’A的所有属性(数据属性和函数属性),实现代码重用
注意:
当然子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了。
子类调用父类函数功能时
在子类中,新建的重名的函数属性,在编辑函数内功能的时候,有可能需要重用父类中重名的那个函数功能,应该是调用普通函数的方式,即:类名.func(),此时就与调用普通函数无异了,因此即便是self参数也要为其传值。
class Hero: def __init__(self,nickname,aggressivity,life_value): self.nickname=nickname self.aggressivity=aggressivity self.life_value=life_value def attack(self,enemy): enemy.life_value-=self.aggressivity class Riven(Hero): camp='Noxus' def __init__(self,nickname,aggressivity,life_value,skin): Hero.__init__(self,nickname,aggressivity,life_value) #调用父类__init__功能 # super(Riven,self).__init__(nickname,aggressivity,life_value) 调用父类__init__方法,推荐使用
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.3 组合与重用性
软件重用的重要方式除了继承之外还有另外一种方式,即:组合
组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合
#类的组合 class Teacher: def __init__(self,name,sex,course): self.name=name self.sex=sex self.course=course class Course: def __init__(self,name,price,peroid): self.name=name self.price=price self.period=peroid python_obj=Course('python',15800,'7m') t1=Teacher('egon','male',python_obj) print(t1.course.name)
##结果
python
组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合。
组合与继承都是有效地利用已有类的资源的重要方式。但是二者的概念和使用场景皆不同。
1、继承的方式
继承的方式通过继承建立了派生类与基类之间的关系,它是一种'是'的关系,比如白马是马,人是动物。
当类之间有很多相同的功能,提取这些共同的功能做成基类,用继承比较好,比如教授是老师
2、组合的方式
用组合的方式建立了类与组合的类之间的关系,它是一种‘有’的关系, 比如教授有生日,教授教python课程
3.4 接口与归一化设计
接口的特征
1)是一组功能的集合,而不是一个功能
2)接口的功能用于交互,所有的功能都是public,即别的对象可操作
3)接口只定义函数,但不涉及函数实现
4)这些功能是相关的
继承有两种用途:
一:继承基类的方法,并且做出自己的改变或者扩展(代码重用、派生和组合)
二:声明某个子类兼容于某基类,定义一个接口类Interface,接口类中定义了一些接口名(就是函数名)且并未实现接口的功能,子类继承接口类,并且实现接口中的功能
实践中,继承的第一种含义意义并不很大,甚至常常是有害的,因为它使得子类与基类出现强耦合。
继承的第二种含义非常重要,它又叫“接口继承”。
接口继承实质上是要求“做出一个良好的抽象,这个抽象规定了一个兼容接口,使得外部调用者无需关心具体细节,可一视同仁的处理实现了特定接口的所有对象”——这在程序设计上,叫做归一化。
python中没有关键字来定义一个接口,使用继承来模仿接口。
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(All_file): def read(self): print('进程数据的读取方法') def write(self): print('进程数据的读取方法')
3.5 抽象类
抽象类:本质还是类,与普通类额外的特点的是:加了装饰器的函数,子类必须实现他们
抽象类是一个特殊的类,它的特殊之处在于只能被继承,不能被实例化
为什么要有抽象类
如果说类是从一堆对象中抽取相同的内容而来的,那么抽象类就是从一堆类中抽取相同的内容而来的,内容包括数据属性和函数属性。
从设计角度去看,如果类是从现实对象抽象而来的,那么抽象类就是基于类抽象而来的。
从实现角度来看,抽象类与普通类的不同之处在于:抽象类中只能有抽象方法(没有实现功能),该类不能被实例化,只能被继承,且子类必须实现抽象方法。这一点与接口有点类似,但其实是不同的,即将揭晓答案。
抽象类与接口
抽象类的本质还是类,指的是一组类的相似性,包括数据属性和函数属性,而接口只强调函数属性的相似性。
抽象类是一个介于类和接口直接的一个概念,同时具备类和接口的部分特性,可以用来实现归一化设计
#_*_coding:utf-8_*_ #一切皆文件 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)
3.6 继承实现的原理(继承顺序)
一、继承顺序
1、python的类可以继承多个类,java和C#中则只能继承一个类
2、python的类如果继承了多个类,那么其寻找的方式有两种,分别是:深度优先和广度优先
当类是经典类时,多继承情况下,会按照深度优先的方式查找,一个分支走到头查找
当类是新式类时,多继承情况下,会按照广度优先的方式查找
class D(object) class C1(D) class C2(D) class B(C1,C2) class A(B,C1,C2): 新式类继承查找顺序是:A->B->C1->C2->D->object(按离A得远近) 经典类的继承的查找顺序是:从左向右,深度优先 上面的class A(B,C1,C2)查找顺序是:A->B->C1->D->object->C2
二、新式类和经典类
# 经典类 class c1: pass class c2(c1): pass # 新式类 class c1(object): pass class c2(c1): pass #python3中统一都是新式类 #pyhon2中才分新式类与经典类
三、继承原理
python到底是如何实现继承的,对于你定义的每一个类,python会计算出一个方法解析顺序(MRO)列表,这个MRO列表就是一个简单的所有基类的线性顺序列表。
查看基类的线性顺序列表:类名.__mro__
只有新式才有这个属性可以查看线性列表,经典类没有这个属性
四 多态与多态性
4.1 多态
多态指的是一类事物有多种形态,(一个抽象类有多个子类,因而多态的概念依赖于继承)
文件有多种形态:文件文件,可执行文件
import abc class File(metaclass=abc.ABCMeta): #同一类事物:文件 @abc.abstractmethod def click(self): pass class Text(File): #文件的形态之一:文本文件 def click(self): print('open file') class ExeFile(File): #文件的形态之二:可执行文件 def click(self): print('execute file')
4.2 多态性
多态性:一种调用方式,不同的执行效果(多态性)
多态性是指具有不同功能的函数可以使用相同的函数名,这样就可以用一个函数名调用不同内容的函数。
多态性依赖于:继承
多态性:定义统一的接口
class Animal:
def run(self):
raise AttributeError('子类必须实现这个方法')
class People(Animal):
def run(self):
print('人正在走')
class Pig(Animal):
def run(self):
print('pig is walking')
class Dog(Animal):
def run(self):
print('dog is running')
peo1=People()
pig1=Pig()
d1=Dog()
peo1.run()
pig1.run()
d1.run()
多态性的好处
1.增加了程序的灵活性:以不变应万变,不论对象千变万化,使用者都是同一种形式去调用,如func(animal)
2.增加了程序额可扩展性:通过继承animal类创建了一个新的类,使用者无需更改自己的代码,还是用func(animal)去调用
五 封装
5.1 为什么要封装
封装数据的主要原因是:保护隐私
封装方法的主要原因是:隔离复杂度
5.2 封装分为两个层面
第一个层面的封装:
创建类和对象会分别创建二者的名称空间,我们只能用类名.或者obj.的方式去访问里面的名字,这本身就是一种封装。
注意:对于这一层面的封装(隐藏),类名.和实例名.就是访问隐藏属性的接口
第二个层面的封装:
类中把某些属性和方法隐藏起来(或者说定义成私有的),只在类的内部使用、外部无法访问,或者留下少量接口(函数)供外部访问。
在python中属性、方法用双下划线的方式实现隐藏属性(设置成私有的,只能内部调用)
类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式:
这种自动变形的特点:
1.类中定义的__x只能在内部使用,如self.__x,引用的就是变形的结果。
#内部定义内部调用 class A: def __init__(self): self.__x = 1 def tell(self): print(self.__x) #内部可以用__x调用 a = A() a.tell()
2.这种变形其实正是针对外部的变形,在外部是无法通过__x这个名字访问到的。
3.在子类定义的__x不会覆盖在父类定义的__x,因为子类中变形成了:_子类名__x,而父类中变形成了:_父类名__x,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的。
注意:对于这一层面的封装(隐藏),我们需要在类中定义一个函数(接口函数)在它内部访问被隐藏的属性,然后外部就可以使用了。
这种变形需要注意的问题是:
1.这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问。还可以用property进行伪装调用。
#内部定义外部调用 class A: __x=1 #_A__x def __test(self):#_A__test print("from A") print(A._A__x) A._A__test(111)
class A:
def __init__(self):
self.__name="kivi"
@property
def name(self):
return self.__name
a=A()
a.name
2.__名字,这种语法,只在定义的时候才会有变形的效果,如果类和对象已经产生,就不会有变形效果
# 类已经产生 class B: pass B.__x = 1 print(B.__dict__) #__x:1 print(B.__x) #对象已经产生 class B: pass b = B() b.__x =1 print(b.__dict__) #__x:1 print(b.__x)
3.在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的。
#正常情况 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() #b.test()-->B-->A--> b.fa() # from B #把fa定义成私有的,即__fa class A: def __fa(self): #在定义时就变形为_A__fa print('from A') def test(self): self.__fa() #只会与自己所在的类为准,即调用self._A__fa class B(A): def __fa(self): print('from B') b=B() b.test() # from A
python要想与其他编程语言一样,严格控制属性的访问权限,只能借助内置方法如__getattr__。