一、什么是面向对象的程序设计及为什么要有他
面向对象的程序设计的核心是对象,要理解对象为何物,必须把自己当成上帝,上帝眼里世间存在的万物皆为对象,不存在的也可以创造出来。
优点:
解决了程序的扩展性,对某一个对象单独修改,会立刻反应到整个体系中,如对游戏中一个人物参数的特征和技能修改都很容易。
缺点:
可控性差,无法像面向过程的程序设计流水线式的可以很精准的预测问题的处理流程与结果,面向对象的程序一旦开始就由对象之间的交互解决问题,即便是上帝也无法预测最终结果。于是我们经常看到一个游戏英雄某一参数的修改极有可能导致硬霸的技能出现,一刀砍死好几个人,这个游戏就市区平衡。
应用场景:需求经常变化的软件,一般需求的变化都集中在用户层,互联网应用,企业内部软件,游戏等都是面向对象的程序设计大显身手的好地方
面向对象的程序设计并不是全部,对于一个软件质量来说,面向对象的程序设计只是用来解决扩展性
二、类和对象
1.什么是类和对象
python中一切皆为对象,类型就是类,对象是特征(变量)与技能(函数)的结合体,而类是一系列对象共同的特征与技能的结合体。
2.类相关知识
(1)在python中声明函数与声明类很相似
声明函数
def functionname(args): '函数文档字符串' 函数体
声明类
1 ''' 2 class 类名: 3 '类的文档字符串' 4 类体 5 ''' 6 7 #我们创建一个类 8 class Data: 9 pass
(2)类有两种作用:属性引用和实例化
创建一个类
class Garen: #定义英雄盖伦的类,不同的玩家可以用它实例出自己英雄; camp='Demacia' #所有玩家的英雄(盖伦)的阵营都是Demacia; def attack(self,enemy): #普通攻击技能,enemy是敌人; enemy.life_value-=self.aggressivity #根据自己的攻击力,攻击敌人就减掉敌人的生命值。
1)属性引用
>>> Garen.camp #引用类的数据属性,该属性与所有对象/实例共享 'Demacia' >>> Garen.attack #引用类的函数属性,该属性也共享 <function Garen.attack at 0x101356510> >>> Garen.name='Garen' #增加属性 >>> del Garen.name #删除属性
2)实例化
类名加括号就是实例化,会自动触发__init__函数的运行,可以用它来为每个实例定制自己的特征
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 #根据自己的攻击力,攻击敌人就减掉敌人的生命值。
实例化:类名+括号
>>> g1=Garen('草丛伦') #就是在执行Garen.__init__(g1,'草丛伦'),然后执行__init__内的代码g1.nickname=‘草丛伦’等
self的作用是在实例化时自动将对象/实例本身传给__init__的第一个参数,self可以是任意名字,但是瞎几把写别人就看不懂了。
这种自动传递的机制还体现在g1.attack的调用上,后续会介绍。
查看类的属性
一:我们定义的类的属性到底存到哪里了?有两种方式查看 dir(类名):查出的是一个名字列表 类名.__dict__:查出的是一个字典,key为属性名,value为属性值 二:特殊的类属性 类名.__name__# 类的名字(字符串) 类名.__doc__# 类的文档字符串 类名.__base__# 类的第一个父类(在讲继承时会讲) 类名.__bases__# 类所有父类构成的元组(在讲继承时会讲) 类名.__dict__# 类的字典属性 类名.__module__# 类定义所在的模块 类名.__class__# 实例对应的类(仅新式类中)
(3)对象相关知识
>>> g1=Garen('草丛伦') #类实例化得到g1这个实例,基于该实例我们讲解实例相关知识 >>> type(g1) #查看g1的类型就是类Garen <class '__main__.Garen'> >>> isinstance(g1,Garen) #g1就是Garen的实例 True
1)对象只有一种作用:属性引用
#对象/实例本身其实只有数据属性 >>> g1.nickname '草丛伦' >>> g1.aggressivity 58 >>> g1.life_value 455 ''' 查看实例属性 同样是dir和内置__dict__两种方式 特殊实例属性 __class__ __dict__ .... '''
对象/实例本身只有数据属性,但是python的class机制会将类的函数绑定到对象上,称为对象的方法,或者叫绑定方法,绑定方法唯一绑定一个对象,同一个类的方法绑定到不同的对象上,属于不同的方法,内存地址都不会一样
>>> g1.attack #对象的绑定方法 <bound method Garen.attack of <__main__.Garen object at 0x101348dd8>> >>> Garen.attack #对象的绑定方法attack本质就是调用类的函数attack的功能,二者是一种绑定关系 <function Garen.attack at 0x101356620>
对象的绑定方法的特别之处在于:obj.func()会把obj传给func的第一个参数。
(4)对象之间的交互
我们可以仿照garen类再创建一个Riven类
class Riven: camp='Noxus' #所有玩家的英雄(锐雯)的阵营都是Noxus; def __init__(self,nickname,aggressivity=54,life_value=414): #英雄的初始攻击力54; self.nickname=nickname #为自己的锐雯起个别名; self.aggressivity=aggressivity #英雄都有自己的攻击力; self.life_value=life_value #英雄都有自己的生命值; def attack(self,enemy): #普通攻击技能,enemy是敌人; enemy.life_value-=self.aggressivity #根据自己的攻击力,攻击敌人就减掉敌人的生命值。
实例出一个Riven来
>>> r1=Riven('锐雯雯')
交互:锐雯雯攻击草丛伦,反之一样
>>> g1.life_value 455 >>> r1.attack(g1) >>> g1.life_value 401
(5)类名称空间与对象名称空间
创建一个类就会创建一个类的名称空间,用来存储类中定义的所有名字,这些名字称为类的属性
而类有两种属性:数据属性和函数属性
其中类的数据属性是共享给所有对象的
>>> id(r1.camp) #本质就是在引用类的camp属性,二者id一样 4315241024 >>> id(Riven.camp) 4315241024
而类的函数属性是绑定到所有对象的:
>>> id(r1.attack) 4302501512 >>> id(Riven.attack) 4315244200 ''' r1.attack就是在执行Riven.attack的功能,python的class机制会将Riven的函数属性attack绑定给r1,r1相当于拿到了一个指针,指向Riven类的attack功能 除此之外r1.attack()会将r1传给attack的第一个参数 '''
创建一个对象/实例就会创建一个对象/实例的名称空间,存放对象/实例的名字,称为对象/实例的属性
在obj.name会先从obj自己的名称空间里找name,找不到则去类中找,类也找不到就找父类...最后都找不到就抛出异常
三、继承与派生
1.什么是继承
继承是用来创建新的类的一种方式,好处是解决代码重用的问题,减少代码冗余
继承是类与类之间的关系,是一种什么‘是’什么的关系
python中类的继承分为:单继承和多继承
class ParentClass1: #定义父类 pass
class ParentClass2: #定义父类 pass
class SubClass1(ParentClass1): #单继承,基类是ParentClass1,派生类是SubClass pass
class SubClass2(ParentClass1,ParentClass2): #python支持多继承,用逗号分隔开多个继承的类 pass
查看继承
SubClass1.__bases__ #__base__只查看从左到右继承的第一个子类,__bases__则是查看所有继承的父类 (<class '__main__.ParentClass1'>,) SubClass2.__bases__ (<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)
提示:如果没有指定基类,python的类会默认继承object类,object是所有python类的基类,它提供了一些常见方法(如__str__)的实现。
>>> ParentClass1.__bases__ (<class 'object'>,) >>> ParentClass2.__bases__ (<class 'object'>,)
class People: #定义父类 def __init__(self,name,age,sex): self.name=name self.age=age self.sex=sex def walk(self): print('%s is walking' %self.name) def foo(self): print('from father %s' %self.name) class Teachar(People): #单继承,People是基类,Teachar是派生类 school = 'oldboy' def __init__(self,name,age,sex,level,salary): People.__init__(self,name,age,sex) self.level=level self.salary=salary def teach(self): print('%s is teaching' %self.name) def foo(self): People.foo(self) print('from teacher') class Student(People): def __init__(self,name,age,sex,group): People.__init__(self,name,age,sex) self.group=group def study(self): print('%s is studying' %self.name) t=Teachar('sam',18,'male',10,3000)
组合
class People: def __init__(self,name,age,year,mon,day): self.name=name self.age=age self.birth=Date(year,mon,day) def walk(self): print('%s is walking' %self.name) class Date: def __init__(self,year,mon,day): self.year=year self.mon=mon self.day=day def tell_birth(self): print('出生于<%s>年<%s>月<%s>日' %(self.year,self.mon,self.day)) class Teachar(People): def __init__(self,name,age,year,mon,day,level,salary): People.__init__(self,name,age,year,mon,day) self.level=level self.salary=salary def teach(self): print('%s is teaching' %self.name) class Student(People): def __init__(self,name,age,year,mon,day,group): People.__init__(self,name,age,year,mon,day) self.group=group def study(self): print('%s is studying' %self.name)
抽象类
import abc #利用abc模块实现抽象类 class File(metaclass=abc.ABCMeta): @abc.abstractmethod #定义一个抽象类,无需实现功能 def read(self):
#子类必须有read功能 pass @abc.abstractmethod def write(self):
#子类必须有write功能 pass class Txt(File): def read(self): #子类继承抽象类,但必须定义read和write方法 pass def write(self): pass t=Txt()
继承实现的原理
1.继承顺序
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中才分新式类与经典类 继承顺序
2.继承原理
python是通过一个叫做mro的算法解析出顺序列表,这个列表就是一个简单的所有基类的线性顺序列表例如:
[<class '__main__.H'>, <class '__main__.E'>, <class '__main__.B'>, <class '__main__.F'>, <class '__main__.C'>, <class '__main__.G'>, <class '__main__.D'>, <class '__main__.A'>, <class 'object'>]
python会在mro列表上从左到右开始查找基类,知道找到第一个匹配这个属性的类为止。
而这个mro列表的构造是通过一个c3线性化算法来实现的,我们不去深究这个算法的数学原理,它实际上就是合并所有父类的mro列表并遵循下列三条准则:
1.子类会先于父类被检查
2.多个父类会根据它们在列表中的顺序被检查
3.如果下一个类存在两个合法的选择,选择第一个父类
子类中调用父类方法
方法一:父类名.父类方法()
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()
class People: def __init__(self,name,age,sex): self.name=name self.age=age self.sex=sex def foo(self): print('from parent') class Teacher(People): def __init__(self,name,age,sex,level,salary): #在python3中 super().__init__(name,age,sex)#调用父类的__init__功能,实际上用的是绑定方法 #在python2中 super(Teacher,self).__init__(name,age,sex) self.level=level self.salary=salary def foo(self): super().foo() print('from child') t=Teacher('egon',18,'male',10,3000) t.foo()
当你使用super()函数是,python会在MRO列表上继续搜索下一个类,只要每个冲定义的方法同意使用super()并之调用它一次,那么控制流最终会遍历完整个MRO列表,每个方法也只会被调用一次(使用super调用的多有属性,都是从MRO列表当前的位置往后找,千万不要通过看代码去找继承关系,一定要看MRO列表)
五、多态与多态性
import abc class Animal(metaclass=abc.ABCMeta): #同一类事物:动物 @abc.abstractmethod def talk(self): pass class People(Animal): #动物的形态之一:人 def talk(self): print('say hello') class Dog(Animal): #动物的形态之二:狗 def talk(self): print('say wangwang') class Pig(Animal): #动物的形态之三:猪 def talk(self): print('say aoao')
2.多态性
(1)什么是多态性
多态性是指具有不同功能的函数可以使用相同的函数名,这样就可以用一个函数名调用不同功能的函数。
(2)为什么要用多态性
1)增加了和程序的灵活性
以不变应万变,不论对象千变万化,使用者都是同一种形式去调用
2)增加了程序的可扩展性
通过继承类穿件了一个新的类,使用者无需更改自己的代码,还是用func(animal)去调用
六、封装
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的形式访问到.
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()
这种自动变形的特点:
1.类种定义的__x只能在内部使用,如self.__x,引用的就是变形的结果。
2.这种变形其实正是针对外部的变形,在外部是无法通过__x这个名字访问到的。
3.在子类定义的__x不会覆盖在父类定义的__x,因为子类中变形成了:_子类名__x,而父类中变形成了:_父类名__x,即双下划线开头的属性在继承给子类时,子类是无法覆盖的。
ps:对于这一层面的封装(隐藏),我们需要在类中定义一个函数(接口函数)在它内部访问被隐藏的属性,然后外部就可以使用了
这种变形需要注意的问题是:
1.这种机制也没有真正意义上限制我们从外部直接访问属性,知道了雷鸣和属性名就可以拼出名字:_类名__属性,然后皆可以访问了,如a._A__N
python并不会真的阻止你访问私有的属性,模块也遵循这种约定,如果模块名以单下划线开头,那么from module import *时不能被导入,但是你from module import _private_module依然是可以导入的其实很多时候你去调用一个模块的功能时会遇到单下划线开头的(socket._socket,sys._home,sys._clear_type_cache),这些都是私有的,原则上是供内部调用的,作为外部的你,一意孤行也是可以用的,只不过显得稍微傻逼一点点
python要想与其他编程语言一样,严格控制属性的访问权限,只能借助内置方法如__getattr__,详见面向对象进阶
例一:BMI指数(bmi是计算而来的,但很明显它听起来像是一个属性而非方法,如果我们将其做成一个属性,更便于理解)
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('sam',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) #同上 ''' 输出结果: 10 314.1592653589793 62.83185307179586 '''
2.为甚要用property
将一个类的函数定义成特性以后,对象再去使用的时候obj.name,根本无法差距自己的name是执行了一个函数后计算出来的,这种特性的使用方式遵循了同意访问的原则,
import pickle class Teacher: school='oldboy' def __init__(self,name,age,sex,year,mon,day,level,salary): self.name=name self.age=age self.sex=sex self.level=level self.salary=salary self.__birth=Date(year,mon,day) @property def birth(self): print('name:%s birth:%s' % (self.name,self.__birth.Birth())) @property def save(self): with open('teacherdb.pkl','ab') as f: pickle.dump(self,f) class Date: def __init__(self,year,mon,day): self.year=year self.mon=mon self.day=day def Birth(self): return '%s-%s-%s' %(self.year,self.mon,self.day) alex=Teacher('alex',84,'male',1990,3,45,1,300000) alex.birth alex.save
4.封装与扩展性
封装在于明确区分内外,使得类实现者可以修改封装内的东西而不影响外部调用者的代码,二外部使用者只知道一个接口(函数),只要接口(函数)名、参数不变,使用者的代码永远无需改变。这就提供一个良好的合作基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑
#类的设计者 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('卧室','sam',20,20,20) >>> r1.tell_area() #使用者调用接口tell_area 400
#类的设计者,轻松的扩展了功能,而类的使用者完全不需要改变自己的代码 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() 8000
七、绑定方法与非绑定方法
类中定义的函数分成两大类:
一、绑定方法(绑定给谁,谁来调用就自动将它本身当作第一个参数传入):
1.绑定到类的方法:用classmethod装饰器装饰的方法,为类量身定制
类.boud_method(),自动将类当作第一个参数传入(其实对象也可调用,但仍将类当作第一个参数传入)
2.绑定到对象的方法:没有被任何装饰器装饰的方法,为对象量身定制
对象.boud_method(),自动将对象当做第一个参数传入
(属于类的函数,类可以调用,但是必须按照函数的规则来,没有自动传值name一说)
二、非绑定方法:用staticmethod装饰器装饰的方法
不与类或对象绑定,类和对象都可以调用,但是没有自动传值的功能,就是一个普通工具而已
ps:在类中直接定义的函数,没有被任何装饰器装饰的,都是绑定到对象的方法,可不是普通函数,对象调用该方法会自动传值,二staticmethod装饰的方法,不管谁来调用,都没有自动传值一说
1.staticmethod
statimethod不与类或对象绑定,谁都可以调用,没有自动传值效果,python为我们内置了函数staticmethod来把类中的函数定义成静态方法
import hashlib import time class MySQL: def __init__(self,host,port): self.id=self.create_id() self.host=host self.port=port @staticmethod def create_id(): #就是一个普通工具 m=hashlib.md5(str(time.clock()).encode('utf-8')) return m.hexdigest() print(MySQL.create_id) #<function MySQL.create_id at 0x0000000001E6B9D8> #查看结果为普通函数 conn=MySQL('127.0.0.1',3306) print(conn.create_id) #<function MySQL.create_id at 0x00000000026FB9D8> #查看结果为普通函数
2.classmethod
classmehtod是给类用的,即绑定到类,类在使用时会将类本身当做参数传给类方法的第一个参数(即便是对象来调用也会将类当作第一个参数传入),python为我们内置了函数classmethod来把类中的函数定义成类方法
import settings import hashlib import time class MySQL: def __init__(self,host,port): self.host=host self.port=port @classmethod def from_conf(cls): print(cls) return cls(settings.HOST,settings.PORT) print(MySQL.from_conf) #<bound method MySQL.from_conf of <class '__main__.MySQL'>> conn=MySQL.from_conf() print(conn.host,conn.port) conn.from_conf() #对象也可以调用,但是默认传的第一个参数仍然是类