Python之面向对象编程
编程范式主要分为面向过程及面向对象。
面向过程(Procedural Programming)
程序从上到下一步步执行,一步步从上到下,从头到尾的解决问题 。基本设计思路就是程序一开始是要着手解决一个大的问题,然后把一个大问题分解成很多个小问题或子过程,这些子过程再执行的过程再继续分解直到小问题足够简单到可以在一个小步骤范围内解决。但这样做有个问题,当程序开头定义了一个值为1的变量,后面的子过程必须依赖这个变量,当修改该变量时所有依赖这个变量的子过程都要更改,假如又有一个其他子程序依赖这个子过程,就会发生一系列的影响。如果写简单的脚本用面向过程的范式简单直接,而在做复杂的任务并且以后需要不断维护时,还是用面向过程最方便。
面向对象(Object-Oriented Programming)
面向对象是通过“类”以及“对象”来创建各种模型来实现对真实世界的描述,使用面向对象编程的原因一方面是因为它可以使程序的维护和扩展变得更简单,并且可以大大提高程序开发效率。并且极大的提高了程序的可读性(维护人员在读面向过程的程序是必须从头开始,而面向过程的程序只需了解对对象的操作就可以了)
无论用什么形式来编程,要记住一下原则:
1.写重复代码是非常不好的低级行为。 如果把一段代码复制、粘贴在程序多个地方调用,日后在对该功能修改时需要把程序中多个地方都改一遍,如果遗漏一处可能导致整个程序的运行出现问题,因此我们在写代码时一定要努力避免写重复的代码,否则相当于给自己挖坑。
2.写的代码需要经常变更 开发正规程序跟写小的脚本(用完一次就扔)的区别是代码需要不停的更改,不是修改bug就是添加新的功能,所以为了方便程序的修改及扩展,写代码一定要遵循易读、易改的原则。
用面向对象的编程范式就可规避上述的坑!
面向对象的核心内容:
Class 类
一个类即是对一类拥有相同属性的对象的抽象、蓝图、原型。在类中定义了这些对象的都具备的属性(variables(data))、共同的方法
Object 对象
一个对象即是一个类的实例化后实例,一个类必须经过实例化后方可在程序中调用,一个类可以实例化多个对象,每个对象亦可以有不同的属性,就像人类是指所有人,每个人是指具体的对象,人与人之前有共性,亦有不同
Encapsulation 封装
在类中对数据的赋值、内部调用对外部用户是透明的,这使类变成了一个胶囊或容器,里面包含着类的数据和方法
Inheritance 继承
一个类可以派生出子类,在这个父类里定义的属性、方法自动被子类继承
Polymorphism 多态
多态是面向对象的重要特性,简单点说:“一个接口,多种实现”,指一个基类中派生出了不同的子类,且每个子类在继承了同样的方法名的同时又对父类的方法做了不同的实现,这就是同一种事物表现出的多种形态。
编程其实就是一个将具体世界进行抽象化的过程,多态就是抽象化的一种体现,把一系列具体事物的共同点抽象出来, 再通过这个抽象的事物, 与不同的具体事物进行对话。
对不同类的对象发出相同的消息将会有不同的行为。比如,你的老板让所有员工在九点钟开始工作, 他只要在九点钟的时候说:“开始工作”即可,而不需要对销售人员说:“开始销售工作”,对技术人员说:“开始技术工作”, 因为“员工”是一个抽象的事物, 只要是员工就可以开始工作,他知道这一点就行了。至于每个员工,当然会各司其职,做各自的工作。
多态允许将子类的对象当作父类的对象使用,某父类型的引用指向其子类型的对象,调用的方法是该子类型的方法。这里引用和调用方法的代码编译前就已经决定了,而引用所指向的对象可以在运行期间动态绑定。
我们现在来比较一下两种编程的范式的区别,现在来做一个吃鸡的游戏,定义角色1和角色2,然后把一些功能写成函数
1 Role ={ 2 1:{'name':'吃鸡小王子', 3 'weapon1':'98k', 4 'weapon2':'Scarl', 5 'life_value':100, 6 'kill_person':0, 7 }, 8 2:{'name':'伏地魔', 9 'weapon1':'98k', 10 'weapon2':'Scarl', 11 'life_value':100, 12 'kill_person':0, 13 }, 14 } 15 def got_shot(role): 16 role['life_value']-=20 #当在定义角色的时候如果把life_value写错,调用该函数就会出问题 17 print('%s got shot!'%role['name']) 18 pass 19 def change_weapon1(role,weapon): 20 role['weapon1']=weapon 21 def change_weapon2(role,weapon): 22 role['weapon2']=weapon 23 got_shot(Role[1])
这就是面向过程的设计思路,至少可以发现这些缺陷,
1.每个角色定义的属性名称是一样的,但属性的命名规则是我们自己定的,没有进行属性合法性检测,如果在新增角色的时候把life_value写成life_calue,在进行程序调用的时候就会出现bug。
2.按理说只有被击中以后调用got_shot函数来减血,但我们可以直接改变role1["life_value"]变量的值来改变血值。因此在这里必须设计成只能通过got_shot来减血,但在上述过程是无法实现的。
3.现在需要给每个角色加一件甲,则需要在在每个角色里放置一个新的属性来存储该角色是三级甲还是二级甲,那就需要更改每个角色的代码来添加新的属性,不符合代码可重复用的属性。
我们用oop的方式来完成这个游戏
1 class Role: #定义类 2 def __init__(self,name,weapon1,weapon2,life_value=100,kill_person=0): # 构造函数,在实例化时进行初始化的工作 3 self.name = name #实例变量(静态属性),作用域是实例本身,未实例化是不能使用。 4 self.weapon1 = weapon1 5 self.weapon2 = weapon2 6 self.life_value = life_value 7 self.kill_person = kill_person 8 def got_shot(self): 9 self.life_value-=20 10 print('%s got shot!'%self.name) 11 def change_weapon1(self,weapon): 12 print("drop%s and get %s" % (self.weapon1, weapon)) 13 self.weapon1 = weapon 14 def change_weapon2(self,weapon): 15 print("drop%s and get %s" % (self.weapon2, weapon)) 16 self.weapon2 = weapon 17 18 role1 = Role("吃鸡小王子","98K","M416") #生成角色(实例化) 19 role2 = Role("伏地魔","Scarl","平底锅") 20 role1.change_weapon1("AK") 21 role1.got_shot()
先不考虑细节,相比面对过程的方法,最直接的优点有下面几点:
1.代码量少了很多
2.角色的属性和功能一目了然,增强了程序的可读性。
代码分析
1 def __init__(self,name,weapon1,weapon2,life_value=100,kill_person=0): 2 self.name = name 3 self.weapon1 = weapon1 4 self.weapon2 = weapon2 5 self.life_value = life_value 6 self.kill_person = kill_person
这一段是构造函数,定义了实例变量。那么既然有实例变量就会有类变量
1 class Role: 2 person = 100 #类变量 3 def __init__(self,name,weapon1,weapon2,life_value=100,kill_person=0): 4 self.name = name 5 self.weapon1 = weapon1 6 self.weapon2 = weapon2 7 self.life_value = life_value 8 self.kill_person = kill_person
person = 100就是类变量,实例变量的作用域只在该实例中,并且只有实例化了才能调用,但是类变量在实例化前是可以调用的,
1 print(Role.person) #可以直接调用类变量
类变量的作用:如果我们建立一个类来描述人,类的属性里面包括姓名,年龄,国籍。可以把类定义成这样
1 class Person: 2 def __init__(self,name,age,nationality="中国",): 3 self.name = name 4 self.age = age 5 self.nationality = nationality 6 p1 = Person("张三",22) 7 p2 = Person("李四",25)
这样的话每个实例都要存个国籍,如果有14亿人的话在内存里就要生成对应的14亿个变量,但是这些变量的值是一样的,没必要这么做,这时候我们就可以用到类变量了
1 class Person: 2 nationality = "中国" 3 def __init__(self,name,age): 4 self.name = name 5 self.age = age 6 self.nationality = nationality 7 p1 = Person("张三",22) 8 print(p1.nationality) 9 p2 = Person("李四",25) 10 p2.nationality = "美国"
大部分人的国籍都是“中国”,是大家公用的属性,偶尔有改国籍的可以直接赋值就行,可以节省开销。要注意的一点是如果实例变量和类变量的变量名是一样的,在程序里的规则是先寻实例变量,如果没有的话在找类变量。
现在我们回过头看游戏的代码,这时候要考虑一个事情,在游戏里我们必须中枪了才能减血,而在代码中我们可以绕过这个功能直接对血值进行更改
1 role1.life_value = 50
这就相当于开了个外挂,直接把敌人的血改了,肯定是不行的,这时候就在定义类的时候要用到私有属性
1 class Role: 2 def __init__(self,name,weapon1,weapon2,life_value=100,kill_person=0): 3 self.name = name 4 self.weapon1 = weapon1 5 self.weapon2 = weapon2 6 self.__life_value_ = life_value 7 self.kill_person = kill_person 8 def show_life(self): #在实例化后调用这个功能可以访问变量的值,而不能在程序中更改值。 9 print("life value is %s"%self.__life_value_)
这时候life_value在程序里是无法调用的, 我们可以在类里定义一个功能来显示私有属性的值。同理,可以在功能前加上__来定义一个私有功能。
析构函数:
在实例释放/销毁的时候执行的,函数,常用于做一些收尾的工作,比如关闭一些数据库连接、关闭已经打开的文件路径。在游戏中,如果角色死亡,可用析构函数释放内存。
1 def __del__(self): 2 print("%s is dead!"%self.name)
析构函数的定义就是def __del(self):
未定义析构函数是,程序执行默认的析构函数。如果程序未调用该函数,在程序执行完毕后自动调用该函数。
面向对象的特性
1.封装
封装就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
2.继承
继承就是以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”。被继承的类称为“基类”、“父类”或“超类”。我们定义一个“人”的父类,功能有说话,吃饭,睡觉,然后再定义两个子类:男人、女人,在继承了“人”这个父类的功能后,男人类的功能在加上工作,女人类的功能加上烹饪。
1 class Person: #定义父类 2 def __init__(self,name,age): 3 self.name = name 4 self.age = age 5 def talk(self): 6 print("%s is talking!"%self.name) 7 def eat(self): 8 print("%s is eating!" % self.name) 9 class Man(Person): #定义子类“Man并继承父类的功能 10 def work(self): 11 print("%s is working!" % self.name) 12 class Woman(Person): #定义子类”Woman“并继承父类的功能 13 def cook(self): 14 print("%s is cooking!" % self.name) 15 man = Man("Jack",20) 16 woman = Woman("Rosd",20) 17 man.talk() 18 man.work() 19 woman.talk() 20 woman.cook()
就完成了类的继承属性,但是还有一点,我们需要对父类的功能进行一些修改,Man的eat功能后加上drink的功能,方法一是直接在子类定义这个功能
1 class Man(Person): 2 def eat(self): 3 print("%s is eating!" % self.name) 4 print("%s is drunk!" % self.name)
还有一个方法就是直接把父类的功能拿过来,然后加上新加的功能。
但是在定义子类时,如果增加了新的属性的话,要把父类的属性传给子类。比方我们给Man加一个体重的属性
1 class Man(Person): 2 def __init__(self,name,age,weight): 3 #Person.__init__(self,name,age) #方法1(经典类写法) 4 super(Man,self).__init__(name,age) #方法2(新式类写法) 5 self.weight = weight
其中写法1和2效果是一样的,方法2好处就是如果定义该子类的时候的Class Man(Person)时,或者是同时继承多个类的时候只写一行代码就行了,代码更简化(新式类的写法)。方法1是经典类的写法
新式类的多继承
我们先看下新式类是怎么定义的
1 class Person: #经典类 2 class Person(object): #新式类
现在重新定义一个”关系“类,子类继承Person和Relationship两个类的功能
1 class Person(object): #新式类 2 def __init__(self,name,age): 3 self.name = name 4 self.age = age 5 class Relation(object): 6 def friendship(self,obj): 7 print("%s is making friends with %s"%(self.name,obj.name)) 8 class Man(Person,Relation): 9 def __init__(self,name,age,weight): 10 super(Man,self).__init__(name,age) 11 self.weight = weight 12 def eat(self): 13 Person.eat(self) 14 print("%s is drunk!" % self.name) 15 def work(self): 16 print("%s is working!" % self.name) 17 class Woman(Person): 18 def cook(self): 19 print("%s is cooking!" % self.name) 20 man = Man("Jack",20,60) 21 woman = Woman("Rosd",20) 22 man.friendship(woman)
运行结果:
Jack is making friends with Rosd
在类的多继承时,一定要注意继承的顺序策略。继承的策略分为深度优先和广度优先
我们做一个这样的效果:建立四个类,B和C继承了A类,而D继承了B和C。
1 class A: 2 def __init__(self): 3 print("in A") 4 class B(A): 5 def __init__(self): 6 print("in B") 7 class C(A): 8 def __init__(self): 9 print("in C") 10 class D(B,C): 11 pass 12 obj=D()
运行结果:
in B
而当我们把B里的构造函数去掉后,
1 class A: 2 def __init__(self): 3 print("in A") 4 class B(A): 5 pass 6 # def __init__(self): #屏蔽掉B中的构造函数 7 # print("in B") 8 class C(A): 9 def __init__(self): 10 print("in C") 11 class D(B,C): 12 pass 13 obj=D()
运行结果:
in C
而在将B和C的构造函数全屏蔽时,会出现这样的结果:
1 class A: 2 def __init__(self): 3 print("in A") 4 class B(A): 5 pass 6 # def __init__(self): 7 # print("in B") 8 class C(A): 9 pass 10 # def __init__(self): 11 # print("in C") 12 class D(B,C): 13 pass 14 obj=D()
运行结果:
in A
这种方式先从B寻,B中没有从C中寻,当B和C都没有的时候在从B继承的A中找这种一层一层继承的策略叫广度优先。而另外一种是先从B继承,B没有的话从B继承的A中继承,A也没有的时候从C中继承。这种纵向的继承策略叫深度优先。一般情况下深度优先的执行效率不及广度优先的效率高。
在Python2中经典类是按照深度优先来继承,新式类是广度优先的继承策略;而在Python3中经典类和新式类都是按照广度优先的继承策略。
3.多态(Polymorphism)
接口的多种不同的实现方式即为多态。就是一个接口可以实现不同的效果。
1 class Animal: 2 def __init__(self,name): 3 self.name = name 4 def talk(self,obj): 5 obj.talk() 6 class Dog(Animal): 7 def talk(self): 8 print("%s:woof"%self.name) 9 class Cat(Animal): 10 def talk(self): 11 print("%s:Meow"%self.name) 12 13 d = Dog("Goofy") 14 c = Cat("Tom") 15 16 def animal_talk(obj): 17 obj.talk() 18 animal_talk(d) 19 animal_talk(c)
执行结果:
Goofy:woof
Tom:Meow
在这段代码中,animal_talk使用了一个接口实现了不同功能的调用,就是一个多态的函数。