1.面向过程编程VS面向对象编程
面向过程编程(Procedural Programming):就是程序从上到下一步步执行,一步步从上到下,从头到尾的解决问题 。基本设计思路就是程序一开始是要着手解决一个大的问题,然后把一个大问题分解成很多个小问题或子过程,这些子过程再执行的过程再继续分解直到小问题足够简单到可以在一个小步骤范围内解决。
优点:将复杂的问题一步步分解,简化问题的复杂度
应用场景:面向过程的程序设计思想一般用于那些功能一旦实现之后就很少需要改变的场景, 如果你只是写一些简单的脚本,去做一些一次性任务,用面向过程的方式是极好的,著名的例子有Linux內核,git,以及Apache HTTP Server等。但如果你要处理的任务是复杂的,且需要不断迭代和维护 的, 那还是用面向对象最方便了。
面向对象编程(Object Oriented Programing):面向对象编程(OOP)编程是利用“类”和“对象”来创建各种模型来实现对真实世界的描述。
优点:1.使程序更加容易扩展和易更改,使开发效率变的更高。
2.基于面向对象的程序可以使它人更加容易理解你的代码逻辑,从而使团队开发变得更从容。
应用场景:当然是应用于需求经常变化的软件中,一般需求的变化都集中在用户层,互联网应用,企业内部软件,游戏等都是面向对象的程序设计大显身手的好地方。
2.类与对象
2.1名词解释
- 类:一个类即是对一类拥有相同属性的对象的抽象、蓝图、原型、模板。在类中定义了这些对象的都具备的属性(variables(data))、共同的方法
- 属性:人类包含很多特征,把这些特征用程序来描述的话,叫做属性,比如年龄、身高、性别、姓名等都叫做属性,一个类中,可以有多个属性
- 方法:人类不止有身高、年龄、性别这些属性,还能做好多事情,比如说话、走路、吃饭等,相比较于属性是名词,说话、走路是动词,这些动词用程序来描述就叫做方法。
- 实例(对象):一个对象即是一个类的实例化后实例,一个类必须经过实例化后方可在程序中调用,一个类可以实例化多个对象,每个对象亦可以有不同的属性,就像人类是指所有人,每个人是指具体的对象,人与人之前有共性,亦有不同
- 实例化:把一个类转变为一个对象的过程就叫实例化
2.2类与对象
2.2.1类与类的操作
定义类(类的命名、属性、方法)
class FactoryStaff: # 定义类,类名首字母大写 factory = '富士康' # 类的属性 def move(self): # 类的方法 print('搬运中...') print('搬运结束') def rest(self): print('休息中...') print('休息结束')
操作类
# 类的增操作FactoryStaff.industry = '制造业'print(FactoryStaff.__dict__)
# 类的删操作del FactoryStaff.industryprint(FactoryStaff.__dict__)
# 类的改操作FactoryStaff.industry = '手工业'print(FactoryStaff.__dict__)
注意:
- 类中可以有任意python代码,这些代码在类定义阶段便会执行,因而会产生新的名称空间,用来存放类的变量名与函数名,可以通过FactoryStaff.__dict__来查看
- 类中定义的名字,都是类的属性,点是访问属性的语法。
- 对于经典类来说我们可以通过该字典操作类名称空间的名字,但新式类有限制
2.2.2对象与对象的操作
创建(实例化)对象
# 实例化对象Staff1 = FactoryStaff() # Staff1是类的实例,此过程是实例化print(Staff1)
为对象添加独有属性
class FactoryStaff: # 定义类,类名首字母大写 factory = '富士康' # 类的属性 def __init__(self, name, sex, age): # 定义每个对象独有的属性 self.Name = name self.Sex = sex self.Age = age def move(self): # 类的方法 print('搬运中...') print('搬运结束') def rest(self): print('休息中...') print('休息结束')Staff1 = FactoryStaff('张三丰', '男', 50)print(Staff1.__dict__)
在定义类中添加了__init__()方法,运行结果如下:
对象操作
# 对象的增操作Staff1.hobby = '太极'print(Staff1.__dict__)
# 对象的删操作del Staff1.hobbyprint(Staff1.__dict__)
# 对象的改操作Staff1.hobby = '足球'print(Staff1.__dict__)
注意:
- 站的角度不同,定义出的类是截然不同的;
- 现实中的类并不完全等于程序中的类,比如现实中的公司类,在程序中有时需要拆分成部门类,业务类等;
- 有时为了编程需求,程序中也可能会定义现实中不存在的类,比如策略类,现实中并不存在,但是在程序中却是一个很常见的类。
2.3属性查找与绑定方法
2.3.1属性查找
类有两种属性:数据属性和函数属性
1、类的数据属性是所有对象共享的
#类的数据属性是所有对象共享的,id都一样print(id(FactoryStaff.factory)) # 4377347328print(id(Staff1.factory)) # 4377347328print(id(Staff2.factory)) # 4377347328print(id(Staff3.factory)) # 4377347328
2、类的函数数据是绑定给对象用的,称为绑定到对象的方法
#类的函数属性是绑定给对象使用的,obj.method称为绑定方法,内存地址都不一样print(FactoryStaff.move) #print(Staff1.move) # >print(Staff2.move) # >print(Staff3.move) # >#id是python的实现机制,并不能真实反映内存地址,如果有内存地址,还是以内存地址为准
在obj.name会先从obj自己的名称空间里找name,找不到则去类中找,类也找不到就找父类...最后都找不到就抛出异常
2.3.2绑定方法
首先,定义类并实例出三个对象
class FactoryStaff: # 定义类,类名首字母大写 factory = '富士康' # 类的属性 def __init__(self, name, sex, age): # 定义每个对象独有的属性 self.Name = name self.Sex = sex self.Age = age def move(self): # 类的方法 print('%s正在搬运中...' % self.Name) print('搬运结束') def rest(self): print('%s休息中...' % self.Name) print('休息结束')Staff1 = FactoryStaff('张一丰', '男', 50)Staff2 = FactoryStaff('张二丰', '男', 40)Staff3 = FactoryStaff('张三丰', '男', 30)
类中定义的函数(没有被任何装饰器装饰的),其实主要是给对象使用的,而且是绑定到对象的,虽然所有对象指向的都是相同的功能,但是绑定到不同的对象就是不同的绑定方法
强调:绑定到对象的方法的特殊之处在于,绑定给谁就由谁来调用,谁来调用,就会将‘谁’本身当做第一个参数传给方法,即自动传值(方法__init__也是一样的道理)如下:
Staff1.move() # 等价于FactoryStaff.move(Staff1)Staff2.rest() # 等价于FactoryStaff.rest(Staff2)FactoryStaff.move(Staff3) # 验证一下
运行结果:
注意:绑定到对象的方法的这种自动传值的特征,决定了在类中定义的函数都要默认写一个参数self,self可以是任意名字,但是约定俗成地写出self。
Practice:
1.编写一个学生类,产生一堆学生对象。 要求:有一个计数器(属性),统计总共实例了多少个对象。
class Student: School = '北理工' count = 0 def __init__(self, name, age, sex): self.name = name self.age = age self.sex = sex Student.count += 1 # 每次实例化的时候,自动执行__init__,count自动加1 def learn(self): print('%s is learning' % self.name)print(Student.count) # 0stu1 = Student('张一丰', 18, '男')print(stu1.count) # 1stu2 = Student('张二丰', 28, '女')print(stu2.count) # 2stu3 = Student('张三丰', 38, '男')print(stu1.count) # 3print(stu2.count) # 3print(stu3.count) # 3
2.模仿LoL定义两个英雄类。 要求:英雄需要有昵称、攻击力、生命值等属性;实例化出两个英雄对象;英雄之间可以互殴,被殴打的一方掉血,血量小于0则判定为死亡。
class Hero: game = 'LOL' def __init__(self, name, attack, hp): self.name = name self.attack = attack self.hp = hp def attacking(self, target): target.hp = target.hp - self.attack # 攻击掉血hero1 = Hero('德玛西亚', 80, 1500)hero2 = Hero('寒冰射手', 150, 800)while True: hero1.attacking(hero2) if hero2.hp <= 0: print('%s win!' % hero1.name) break hero2.attacking(hero1) if hero1.hp <= 0: print('%s win!' % hero2.name) break
3.继承、派生、重用
3.1继承
继承指的是类与类之间的关系,是一种什么“是”什么的关系,继承的功能之一就是用来解决代码重用问题
继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,父类又可以成为基类或超类,新建的类称为派生类或子类
接上面练习2中的例子:
class Hero: game = 'LOL' def __init__(self, name, attack, hp): self.name = name self.attack = attack self.hp = hp def attacking(self, target): target.hp = target.hp - self.attack # 攻击掉血class ZhanShi(Hero): # 父类是Hero camp = '战士'class FaShi(Hero): camp = '法师'
查看继承
>>print(ZhanShi.__bases__)(,)>>print(FaShi.__bases__)( ,)
3.2重用与派生
重用
在开发程序的过程中,如果我们定义了一个类A,然后又想新建立另外一个类B,但是类B的大部分内容与类A的相同时,我们不可能从头开始写一个类B,这就用到了类的继承的概念。
通过继承的方式新建类B,让B继承A,B会‘遗传’A的所有属性(数据属性和函数属性),实现代码重用
派生
当然子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了。
class ZhanShi(Hero): camp = '战士' # 派生class FaShi(Hero): camp = '法师' # 派生print(Zhanshi.attacking) # 重用>>
#按属性查找#
举一个简单的例子:
class Foo: def f1(self): print('Foo.f1') def f2(self): print('Foo.f2') self.f1()class Bar(Foo): def f1(self): print('Bar.f1')b=Bar()b.f2()# 打印结果:# Foo.f2# Bar.f1
对象的属性引用,会先从自己的实例中找,然后去类中找,然后再去父类中找...直到最顶级的父类。
本例中,b.f2()会在b中先找绑定方法,然后到Bar类中寻找,最后在父类Foo中找到,执行遇到self.f1(),self的作用是将对象作为参数传入绑定方法中,因此,此处的self.f1等价于b.f1,因此在Bar类中找到并执行。
3.3继承的实现原理
python到底是如何实现继承的,对于你定义的每一个类,python会计算出一个方法解析顺序(MRO)列表,这个MRO列表就是一个简单的所有基类的线性顺序列表,例如:
>>> F.mro() #等同于F.__mro__[, , , , , , ]
为了实现继承,python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:
- 子类会先于父类被检查
- 多个父类会根据它们在列表中的顺序被检查
- 如果对下一个类存在两个合法的选择,选择第一个父类
在Java和C#中子类只能继承一个父类,而Python中子类可以同时继承多个父类,如果继承了多个父类,那么属性的查找方式有两种,分别是:深度优先(经典类)和广度优先(新式类)。
关于经典类和新式类有以下几条需要知道的:
只有在python2中才分新式类和经典类,python3中统一都是新式类
在python2中,没有显式的继承object类的类,以及该类的子类,都是经典类
在python2中,显式地声明继承object的类,以及该类的子类,都是新式类
在python3中,无论是否继承object,都默认继承object,即python3中所有类均为新式类
也就是说,在Python3中,所有的类都是新式类,都默认继承object类,所以Python3中的属性查找方法都是广度优先。
上图中左边是深度优先(查找顺序A->B->E->G(object类)->C->F->D
),右边是广度优先(查找顺序A->B->E->C->F->D->G(object类)
)。
再举一例,有如下代码:
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') passf1=F()f1.test()print(F.__mro__) #只有新式才有这个属性可以查看线性列表,经典类没有这个属性
#新式类继承顺序:F->D->B->E->C->A->object#经典类继承顺序:F->D->B->A->object->E->C#python3中统一都是新式类#pyhon2中才分新式类与经典类
3.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()函数,此方法依赖继承关系,若没有直接继承关系(父子),super仍然会按照mro继续往后查找
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):#摩拜单车 passline13=Subway('中国地铁','180m/s','1000人/箱','电',13)line13.run()
#A没有继承B,但是A内super会基于C.mro()继续往后找class A: def test(self): super().test()class B: def test(self): print('from B')class C(A,B): passc=C()c.test() #打印结果:from Bprint(C.mro())#[, , , ]
4.组合与重用
软件重用的重要方式除了继承之外还有另外一种方式,即:组合
组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合
class Human: school = '北理工' def __init__(self, name, age, sex): self.name = name self.age = age self.sex = sexclass Teacher(Human): def __init__(self, name, age, sex, wages): super().__init__(name, age, sex) self.wages = wages def teach(self): print('%s is teaching ' % self.name)class Student(Human): def __init__(self): pass def learn(self): print('%s is learning' % self.name)class Course: def __init__(self, coursename, price, round): self.course_name = coursename self.price = price self.round = round def show_info(self): print('%s is %s, it will take %s to learn' % (self.course_name, self.price, self.round))python = Course('Python', 3000, '6mons')java = Course('Java', 5000, '8mons')teacher1 = Teacher('张三', '38', '男', 5000)teacher1.course1 = python # 组合teacher1.course2 = javateacher1.course1.show_info() # 可以使用组合的类产生的对象所持有的方法teacher1.course2.show_info()
#组合VS继承#
组合与继承都是有效地利用已有类的资源的重要方式。但是二者的概念和使用场景皆不同,
1.继承的方式
通过继承建立了派生类与基类之间的关系,它是一种'是'的关系,比如白马是马,人是动物。
当类之间有很多相同的功能,提取这些共同的功能做成基类,用继承比较好,比如老师是人,学生是人
2.组合的方式
用组合的方式建立了类与组合的类之间的关系,它是一种‘有’的关系,比如教授有生日,教授教python和linux课程,教授有学生s1、s2、s3...
总结:当类之间有显著不同,并且较小的类是较大的类所需要的组件时,用组合比较好
5.抽象类
5.1接口与归一化设计
java中的interface使用如下
=================第一部分:Java 语言中的接口很好的展现了接口的含义: IAnimal.java/** Java的Interface接口的特征:* 1)是一组功能的集合,而不是一个功能* 2)接口的功能用于交互,所有的功能都是public,即别的对象可操作* 3)接口只定义函数,但不涉及函数实现* 4)这些功能是相关的,都是动物相关的功能,但光合作用就不适宜放到IAnimal里面了 */package com.oo.demo;public interface IAnimal { public void eat(); public void run(); public void sleep(); public void speak();}=================第二部分:Pig.java:猪”的类设计,实现了IAnnimal接口 package com.oo.demo;public class Pig implements IAnimal{ //如下每个函数都需要详细实现 public void eat(){ System.out.println("Pig like to eat grass"); } public void run(){ System.out.println("Pig run: front legs, back legs"); } public void sleep(){ System.out.println("Pig sleep 16 hours every day"); } public void speak(){ System.out.println("Pig can not speak"); }}=================第三部分:Person2.java/**实现了IAnimal的“人”,有几点说明一下: * 1)同样都实现了IAnimal的接口,但“人”和“猪”的实现不一样,为了避免太多代码导致影响阅读,这里的代码简化成一行,但输出的内容不一样,实际项目中同一接口的同一功能点,不同的类实现完全不一样* 2)这里同样是“人”这个类,但和前面介绍类时给的类“Person”完全不一样,这是因为同样的逻辑概念,在不同的应用场景下,具备的属性和功能是完全不一样的 */package com.oo.demo;public class Person2 implements IAnimal { public void eat(){ System.out.println("Person like to eat meat"); } public void run(){ System.out.println("Person run: left leg, right leg"); } public void sleep(){ System.out.println("Person sleep 8 hours every dat"); } public void speak(){ System.out.println("Hellow world, I am a person"); } }=================第四部分:Tester03.javapackage com.oo.demo;public class Tester03 { public static void main(String[] args) { System.out.println("===This is a person==="); IAnimal person = new Person2(); person.eat(); person.run(); person.sleep(); person.speak(); System.out.println("\n===This is a pig==="); IAnimal pig = new Pig(); pig.eat(); pig.run(); pig.sleep(); pig.speak(); } } java中的interface
5.1.1接口
自己提供给使用者来调用自己功能的方式\方法\入口
5.1.2归一化
接口提取了一群类共同的函数,可以把接口当做一个函数的集合,然后让子类去实现接口中的函数。
这么做的意义在于归一化,什么叫归一化,就是只要是基于同一个接口实现的类,那么所有的这些类产生的对象在使用时,从用法上来说都一样。
归一化的好处在于:
- 归一化让使用者无需关心对象的类是什么,只需要的知道这些对象都具备某些功能就可以了,这极大地降低了使用者的使用难度。
- 归一化使得高层的外部使用者可以不加区分的处理所有接口兼容的对象集合
-
- 就好象linux的泛文件概念一样,所有东西都可以当文件处理,不必关心它是内存、磁盘、网络还是屏幕(当然,对底层设计者,当然也可以区分出“字符设备”和“块设备”,然后做出针对性的设计:细致到什么程度,视需求而定)。
- 再比如:我们有一个汽车接口,里面定义了汽车所有的功能,然后由本田汽车的类,奥迪汽车的类,大众汽车的类,他们都实现了汽车接口,这样就好办了,大家只需要学会了怎么开汽车,那么无论是本田,还是奥迪,还是大众我们都会开了,开的时候根本无需关心我开的是哪一类车,操作手法(函数调用)都一样
5.1.2模仿interface
在python中根本就没有一个叫做interface的关键字,如果非要去模仿接口的概念
可以借助第三方模块:
也可以使用继承,其实继承有两种用途
一:继承基类的方法,并且做出自己的改变或者扩展(代码重用):实践中,继承的这种用途意义并不很大,甚至常常是有害的。因为它使得子类与基类出现强耦合。(PS:对象之间的耦合度就是对象之间的依赖性。指导使用和维护对象的主要问题是对象之间的多重依赖性。对象之间的耦合越高,维护成本越高。因此对象的设计应使类和构件之间的耦合最小。)
二:声明某个子类兼容于某基类,定义一个接口类(模仿java的Interface),接口类中定义了一些接口名(就是函数名)且并未实现接口的功能,子类继承接口类,并且实现接口中的功能
class Interface:#定义接口Interface类来模仿接口的概念,python中压根就没有interface关键字来定义一个接口。 def read(self): #定接口函数read pass def write(self): #定义接口函数write passclass 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('进程数据的读取方法')
上面的代码只是看起来像接口,其实并没有起到接口的作用,子类完全可以不用去实现接口 ,这就用到了抽象类。
5.2抽象类
5.2.1 什么是抽象类
与java一样,python也有抽象类的概念但是同样需要借助模块实现,抽象类是一个特殊的类,它的特殊之处在于只能被继承,不能被实例化。
5.2.2 为什么要有抽象类
如果说类是从一堆对象中抽取相同的内容而来的,那么抽象类就是从一堆类中抽取相同的内容而来的,内容包括数据属性和函数属性。
比如我们有香蕉的类,有苹果的类,有桃子的类,从这些类抽取相同的内容就是水果这个抽象的类,你吃水果时,要么是吃一个具体的香蕉,要么是吃一个具体的桃子。。。。。。你永远无法吃到一个叫做水果的东西。
从设计角度去看,如果类是从现实对象抽象而来的,那么抽象类就是基于类抽象而来的。
从实现角度来看,抽象类与普通类的不同之处在于:抽象类中只能有抽象方法(没有实现功能),该类不能被实例化,只能被继承,且子类必须实现抽象方法。这一点与接口有点类似,但其实是不同的。
5.2.3 在python中实现抽象类
需要调用abc模块,继续沿用5.1.2中的例子:
import abcclass Interface(metaclass=abc.ABCMeta): # 定义接口Interface类来模仿接口的概念 @abc.abstractmethod def read(self): #定接口函数read pass @abc.abstractmethod def write(self): #定义接口函数write pass# class Txt(All_file):# pass## t1=Txt() #报错,子类没有定义抽象方法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('进程数据的读取方法')wenbenwenjian=Txt()yingpanwenjian=Sata()jinchengwenjian=Process()#这样大家都是被归一化了,也就是一切皆文件的思想wenbenwenjian.read()yingpanwenjian.write()jinchengwenjian.read()# test = Interface()# >>TypeError:Can't instantiate abstract class Interface with abstract methods read, write # 抽象类不能被实例化
5.2.4 抽象类与接口
抽象类指的是一组类的相似性,它的本质还是类,包括数据属性和函数属性,而接口只强调函数属性的相似性。
抽象类是一个介于类和接口直接的一个概念,同时具备类和接口的部分特性,可以用来实现归一化设计。
6.多态
多态指的是一类事物有多种形态,比如水有冰、水、水蒸气等
6.1.1多态动态绑定(在继承的背景下使用时,有时也称为多态性)
多态性是指在不考虑实例类型的情况下使用实例,多态性分为静态多态性和动态多态性
静态多态性:如任何类型都可以用运算符+进行运算
动态多态性:如下
wenbenwenjian=Txt()yingpanwenjian=Sata()jinchengwenjian=Process()# wenbenwenjian、yingpanwenjian、jinchengwenjian都是文件,只要是文件肯定有read、write方法# 于是我们可以不用考虑它们三者的具体是什么类型,而直接使用wenbenwenjian.read()yingpanwenjian.write()jinchengwenjian.read()#更进一步,我们可以定义一个统一的接口来使用def func(obj): obj.read() obj.write()
6.1.2为什么要用多态性(多态性的好处)
1.增加了程序的灵活性
以不变应万变,不论对象千变万化,使用者都是同一种形式去调用,如func(animal)
2.增加了程序的可扩展性
通过继承animal类创建了一个新的类,使用者无需更改自己的代码,还是用func(animal)去调用
7.封装
7.1隐藏
class A: __arg = '隐藏属性' #类的数据属性就应该是共享的,但是语法上是可以把类的数据属性设置成私有的如__arg,会变形为_A__arg def __f(self): # 变形为_A__f(self) print('这是一个隐藏功能') def show(self): # 只有在类内部才可以通过__f的形式访问到隐藏方法和隐藏属性 print('显示隐藏的功能') print(self.__arg) self.__f()a = A()a.show()#其实这仅仅这是一种变形操作#类中所有双下划线开头的名称如__x都会在定义类的时候自动变形成:_类名__x的形式:
这种自动变形的特点:
- 类中定义的__arg只能在内部使用,如self.__arg,引用的就是变形的结果。
- 这种变形其实正是针对外部的变形,在外部是无法通过__arg这个名字访问到的。
- 在子类定义的__arg不会覆盖在父类定义的__arg,因为子类中变形成了:_子类名__arg,而父类中变形成了:_父类名__arg,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的。
这种变形需要注意的问题是:
- 这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如a._A__arg
- 变形的过程只在类的定义是发生一次,在定义后的赋值操作,不会变形
- 在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的
#封装不是单纯意义的隐藏#
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=aget=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()
提示:在编程语言里,对外提供的接口(接口可理解为了一个入口),可以是函数,称为接口函数,这与接口的概念还不一样,接口代表一组接口函数的集合体。
7.2 封装与扩展性
封装在于明确区分内外,使得类实现者可以修改封装内的东西而不影响外部调用者的代码;而外部使用用者只知道一个接口(函数),只要接口(函数)名、参数不变,使用者的代码永远无需改变。这就提供一个良好的合作基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑。
#类的设计者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()
7.3 property特性
property是一种特殊的属性,访问它时会执行一段功能(函数)然后返回值
例一:BMI指数(bmi是计算而来的,但很明显它听起来像是一个属性而非方法,如果我们将其做成一个属性,更便于理解)
成人的BMI数值:
过轻:低于18.5
正常:18.5-23.9
过重:24-27
肥胖:28-32
非常肥胖, 高于32
体质指数(BMI)=体重(kg)÷身高^2(m)
class People: def __init__(self, name, sex, height, weight): self.name = name self.sex = sex self.__height = height self.__weight = weight @property # 将BMI作为类似类的数据属性的使用 def BMI(self): return self.__weight / (self.__height ** 2) # 使用property时,必须有return语句p1 = People('张三丰', '男', 1.8, 90)print(p1.BMI)
将一个类的函数定义成特性以后,对象再去使用的时候obj.name,根本无法察觉自己的name是执行了一个函数然后计算出来的,这种特性的使用方式遵循了统一访问的原则
也许有的人会说这样子做没有意义,可以直接把静态变量写到初始化方法下的啊,比如下面的例子:
class Circle: def __init__(self,r): self.r = r self.area = pi*self.r**2 self.perimeter = self.r*pi*2c1 = Circle(5)print(c1.area)--78.53981633974483
通过初始化Circle类得到一个圆的对象,半径是5,通过调用他的area方法得到面积,这样似乎是可行的,这时我们再把半径扩大一些,改为10:
c1.r = 10print(c1.area)--78.53981633974483
居然还是78,圆的面积并没有跟着半径一起改过来。这是因为在c1初始化的时候面积和周长就已经计算出来并写入属性名中了,所以我们这里想要实现预想的功能,就必须使用property装饰器。
7.4 property特性与隐藏属性
现有一个需求:某一个属性需要被私有化,又需要能被外部查看,这种情况,把这个属性通过方法返回,方法伪装成属性
class Person: def __init__(self, name): self.__name = name @property def name(self): return self.__nameperson = Person("Jack")print(person.name)-- Jack
如果我们现在有个需求,要改动隐藏属性的值,该怎么做?
在这里直接使用person.name = new_name修改肯定是不行的,根据之前学过的知识,通过person._Person__name = new_name可以将name的值修改,但是既然我们把name属性隐藏起来,肯定是不希望通过这种类似“作弊”的手段来修改的。那么,接下来的办法会很好的解决这一问题。
class Person: def __init__(self, name): self.__name = name @property def name(self): return self.__name @name.setter def name(self, new_name): self.__name = new_name @name.deleter def name(self): del self.__nameperson = Person("Jack")print(person.name)--Jackperson.name = "Alice"print(person.name)--Alicedel person.nameprint(person.name)--AttributeError: 'Person' object has no attribute '_Person__name'# 这里被name.setter和name.deleter装饰器修饰下的name方法,调用方式可以是obj.name = xxx 和 del obj.name, 也可以通过setattr和delattr调用。 # name方法下的代码块可以实现其他功能(即不一定要去修改或删除值,但一般不会这么做) # name.setter装饰器下的方法有且只能有一个参数, name.deleter装饰器下的方法没有参数
8.绑定方法与非绑定方法
类中定义的函数分成两大类:绑定方法和非绑定方法,而绑定方法中,又分为绑定给类的方法和绑定给对象的方法。下面我们就一起来看一下:
8.1 非绑定方法:用staticmethod装饰器装饰的方法
在类内部用staticmethod装饰的函数即非绑定方法,就是普通函数
statimethod不与类或对象绑定,谁都可以调用,没有自动传值效果
import hashlibimport timeclass 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.time()).encode('utf-8')) return m.hexdigest()print(MySQL.create_id) ##查看结果为普通函数conn=MySQL('127.0.0.1',3306)print(conn.create_id) # #查看结果为普通函数
8.2 绑定方法
8.2.1绑定到对象的方法:没有被任何装饰器装饰的方法。
在类中直接定义的函数,没有被任何装饰器装饰的,都是绑定到对象的方法,不是普通函数,对象调用该方法会自动传值,将对象当做第一个参数传入。
8.2.2绑定到类的方法:用classmethod装饰器装饰的方法。
classmehtod是给类用的,即绑定到类,类在使用时会将类本身当做参数传给类方法的第一个参数(即便是对象来调用也会将类当作第一个参数传入),python为我们内置了函数classmethod来把类中的函数定义成类方法。
#settings.pyHOST='127.0.0.1'PORT=3306DB_PATH=r'C:\Users\Administrator\PycharmProjects\test\面向对象编程\test1\db'#test.pyimport settingsclass 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) #>conn=MySQL.from_conf()conn.from_conf() #对象也可以调用,但是默认传的第一个参数仍然是类
#classmethod与staticmethod的对比#
import settingsclass MySQL: def __init__(self,host,port): self.host=host self.port=port @staticmethod def from_conf(): return MySQL(settings.HOST,settings.PORT) # @classmethod #哪个类来调用,就将哪个类当做第一个参数传入 # def from_conf(cls): # return cls(settings.HOST,settings.PORT) def __str__(self): return '就不告诉你'class Mariadb(MySQL): def __str__(self): return '<%s:%s>' %(self.host,self.port)m=Mariadb.from_conf() # 类Mariadb调用from_conf()方法,没有自动去它的父类里面找print(m) #我们的意图是想触发Mariadb.__str__,但是结果触发了MySQL.__str__的执行,打印就不告诉你:
9.元类
9.1知识储备(exec函数的使用)
exec:三个参数参数一:字符串形式的命令参数二:全局作用域(字典形式),如果不指定,默认为globals()参数三:局部作用域(字典形式),如果不指定,默认为locals()
exec的使用
#可以把exec命令的执行当成是一个函数的执行,会将执行期间产生的名字存放于局部名称空间中g={'x':1,'y':2}l={}exec('''global x,zx=100z=200m=300''',g,l)print(g) #{'x': 100, 'y': 2,'z':200,......}print(l) #{'m': 300}
9.2 什么是元类
python中一切皆是对象,类本身也是一个对象,当使用关键字class的时候,python解释器在加载class的时候就会创建一个对象(这里的对象指的是类而非类的实例),因而我们可以将类当作一个对象去使用,同样满足第一类对象的概念,可以:
- 把类赋值给一个变量
- 把类作为函数参数进行传递
- 把类作为函数的返回值
- 在运行时动态地创建类
元类是类的类,是类的模板
元类是用来控制如何创建类的,正如类是创建对象的模板一样,而元类的主要目的是为了控制类的创建行为
元类的实例化的结果为我们用class定义的类,正如类的实例为对象
type是python的一个内建元类,用来直接控制生成类,python中任何class定义的类其实都是type类实例化的对象
9.3 创建类的两种方式
方式一:使用class关键字
class Chinese(object): country='China' def __init__(self,name,age): self.name=name self.age=age def talk(self): print('%s is talking' %self.name)
方式二:就是手动模拟class创建类的过程):将创建类的步骤拆分开,手动去创建
#准备工作:#创建类主要分为三部分 1 类名 2 类的父类 3 类体#类名class_name='Chinese'#类的父类class_bases=(object,)#类体class_body="""country='China'def __init__(self,name,age): self.name=name self.age=agedef talk(self): print('%s is talking' %self.name)"""
步骤一(先处理类体->名称空间):类体定义的名字都会存放于类的名称空间中(一个局部的名称空间),我们可以事先定义一个空字典,然后用exec去执行类体的代码(exec产生名称空间的过程与真正的class过程类似,只是后者会将__开头的属性变形),生成类的局部名称空间,即填充字典。
class_dic={}exec(class_body,globals(),class_dic)print(class_dic)#{'country': 'China', 'talk':, '__init__': }
步骤二:调用元类type(也可以自定义)来产生类Chinense
Foo=type(class_name,class_bases,class_dic) #实例化type得到对象Foo,即我们用class定义的类Fooprint(Foo)print(type(Foo))print(isinstance(Foo,type))'''True'''
我们看到,type 接收三个参数:
- 第 1 个参数是字符串 ‘Foo’,表示类名
- 第 2 个参数是元组 (object, ),表示所有的父类
- 第 3 个参数是字典,这里是一个空字典,表示没有定义属性和方法
补充:若Foo类有继承,即class Foo(Bar):.... 则等同于type('Foo',(Bar,),{})
9.3 自定义元类控制类的行为
#知识储备: #产生的新对象 = object.__new__(继承object类的子类)#步骤一:如果说People=type(类名,类的父类们,类的名称空间),那么我们定义元类如下,来控制类的创建class Mymeta(type): # 继承默认元类的一堆属性 def __init__(self, class_name, class_bases, class_dic): if '__doc__' not in class_dic or not class_dic.get('__doc__').strip(): raise TypeError('必须为类指定文档注释') if not class_name.istitle(): raise TypeError('类名首字母必须大写') super(Mymeta, self).__init__(class_name, class_bases, class_dic)class People(object, metaclass=Mymeta): country = 'China' def __init__(self, name, age): self.name = name self.age = age def talk(self): print('%s is talking' % self.name)#步骤二:如果我们想控制类实例化的行为,那么需要先储备知识__call__方法的使用class People(object,metaclass=type): def __init__(self,name,age): self.name=name self.age=age def __call__(self, *args, **kwargs): print(self,args,kwargs)# 调用类People,并不会出发__call__obj=People('egon',18)# 调用对象obj(1,2,3,a=1,b=2,c=3),才会出发对象的绑定方法obj.__call__(1,2,3,a=1,b=2,c=3)obj(1,2,3,a=1,b=2,c=3) #打印:<__main__.People object at 0x10076dd30> (1, 2, 3) {'a': 1, 'b': 2, 'c': 3}#总结:如果说类People是元类type的实例,那么在元类type内肯定也有一个__call__,会在调用People('egon',18)时触发执行,然后返回一个初始化好了的对象obj#步骤三:自定义元类,控制类的调用(即实例化)的过程class Mymeta(type): #继承默认元类的一堆属性 def __init__(self,class_name,class_bases,class_dic): if not class_name.istitle(): raise TypeError('类名首字母必须大写') super(Mymeta,self).__init__(class_name,class_bases,class_dic) def __call__(self, *args, **kwargs): #self=People print(self,args,kwargs) #('egon', 18) {} #1、实例化People,产生空对象obj obj=object.__new__(self) #2、调用People下的函数__init__,初始化obj self.__init__(obj,*args,**kwargs) #3、返回初始化好了的obj return objclass People(object,metaclass=Mymeta): country='China' def __init__(self,name,age): self.name=name self.age=age def talk(self): print('%s is talking' %self.name)obj=People('egon',18)print(obj.__dict__) #{'name': 'egon', 'age': 18}#步骤四:class Mymeta(type): #继承默认元类的一堆属性 def __init__(self,class_name,class_bases,class_dic): if not class_name.istitle(): raise TypeError('类名首字母必须大写') super(Mymeta,self).__init__(class_name,class_bases,class_dic) def __call__(self, *args, **kwargs): #self=People print(self,args,kwargs) # ('egon', 18) {} #1、调用self,即People下的函数__new__,在该函数内完成:1、产生空对象obj 2、初始化 3、返回obj obj=self.__new__(self,*args,**kwargs) #2、一定记得返回obj,因为实例化People(...)取得就是__call__的返回值 return objclass People(object,metaclass=Mymeta): country='China' def __init__(self,name,age): self.name=name self.age=age def talk(self): print('%s is talking' %self.name) def __new__(cls, *args, **kwargs): obj=object.__new__(cls) cls.__init__(obj,*args,**kwargs) return objobj=People('egon',18)print(obj.__dict__) #{'name': 'egon', 'age': 18}#步骤五:基于元类实现单例模式,比如数据库对象,实例化时参数都一样,就没必要重复产生对象,浪费内存class Mysql: __instance=None def __init__(self,host='127.0.0.1',port='3306'): self.host=host self.port=port @classmethod def singleton(cls,*args,**kwargs): if not cls.__instance: cls.__instance=cls(*args,**kwargs) return cls.__instanceobj1=Mysql()obj2=Mysql()print(obj1 is obj2) #Falseobj3=Mysql.singleton()obj4=Mysql.singleton()print(obj3 is obj4) #True#应用:定制元类实现单例模式class Mymeta(type): def __init__(self,name,bases,dic): #定义类Mysql时就触发 self.__instance=None super().__init__(name,bases,dic) def __call__(self, *args, **kwargs): #Mysql(...)时触发 if not self.__instance: self.__instance=object.__new__(self) #产生对象 self.__init__(self.__instance,*args,**kwargs) #初始化对象 #上述两步可以合成下面一步 # self.__instance=super().__call__(*args,**kwargs) return self.__instanceclass Mysql(metaclass=Mymeta): def __init__(self,host='127.0.0.1',port='3306'): self.host=host self.port=portobj1=Mysql()obj2=Mysql()print(obj1 is obj2)
10.异常处理
异常就是程序运行时发生错误的信号(在程序出现错误时,则会产生一个异常,若程序没有处理它,则会抛出该异常,程序的运行也随之终止),在python中,错误触发的异常如下:
1.语法错误(这种错误,根本过不了python解释器的语法检测,必须在程序执行前就改正)
#语法错误示范一if#语法错误示范二def test: pass#语法错误示范三class Foo pass#语法错误示范四print(haha)
2.逻辑错误
#TypeError:int类型不可迭代for i in 3: pass#ValueErrornum=input(">>: ") #输入helloint(num)#NameErroraaa#IndexErrorl=['egon','aa']l[3]#KeyErrordic={ 'name':'egon'}dic['age']#AttributeErrorclass Foo:passFoo.x#ZeroDivisionError:无法完成计算res1=1/0res2=1+'str'
10.1 异常的种类
在python中不同的异常可以用不同的类型(python中统一了类与类型,类型即类)去标识,一个异常标识一种错误
常见异常
AttributeError 试图访问一个对象没有的树形,比如foo.x,但是foo没有属性xIOError 输入/输出异常;基本上是无法打开文件ImportError 无法引入模块或包;基本上是路径问题或名称错误IndentationError 语法错误(的子类) ;代码没有正确对齐IndexError 下标索引超出序列边界,比如当x只有三个元素,却试图访问x[5]KeyError 试图访问字典里不存在的键KeyboardInterrupt Ctrl+C被按下NameError 使用一个还未被赋予对象的变量SyntaxError Python代码非法,代码不能编译(个人认为这是语法错误,写错了)TypeError 传入对象类型与要求的不符合UnboundLocalError 试图访问一个还未被设置的局部变量,基本上是由于另有一个同名的全局变量,导致你以为正在访问它ValueError 传入一个调用者不期望的值,即使值的类型是正确的
更多异常
ArithmeticErrorAssertionErrorAttributeErrorBaseExceptionBufferErrorBytesWarningDeprecationWarningEnvironmentErrorEOFErrorExceptionFloatingPointErrorFutureWarningGeneratorExitImportErrorImportWarningIndentationErrorIndexErrorIOErrorKeyboardInterruptKeyErrorLookupErrorMemoryErrorNameErrorNotImplementedErrorOSErrorOverflowErrorPendingDeprecationWarningReferenceErrorRuntimeErrorRuntimeWarningStandardErrorStopIterationSyntaxErrorSyntaxWarningSystemErrorSystemExitTabErrorTypeErrorUnboundLocalErrorUnicodeDecodeErrorUnicodeEncodeErrorUnicodeErrorUnicodeTranslateErrorUnicodeWarningUserWarningValueErrorWarningZeroDivisionError
10.2 异常处理
为了保证程序的健壮性与容错性,即在遇到错误时程序不会崩溃,我们需要对异常进行处理.
如果错误发生的条件是可预知的,我们需要用if进行处理:在错误发生之前进行预防:
AGE=10while True: age=input('>>: ').strip() if age.isdigit(): #只有在age为字符串形式的整数时,下列代码才不会出错,该条件是可预知的 age=int(age) if age == AGE: print('you got it') break
如果错误发生的条件是不可预知的,则需要用到try...except:在错误发生之后进行处理:
#基本语法为try: 被检测的代码块except 异常类型: try中一旦检测到异常,就执行这个位置的逻辑#举例try: f=open('a.txt') g=(line.strip() for line in f) print(next(g)) print(next(g)) print(next(g)) print(next(g)) print(next(g))except StopIteration: f.close()
10.3 try..except...详细用法
1.异常类只能用来处理指定的异常情况,如果非指定异常则无法处理
s1 = 'hello'try: int(s1)except IndexError as e: # 未捕获到异常,程序直接报错 print e
2.多分支
s1 = 'hello'try: int(s1)except IndexError as e: print(e)except KeyError as e: print(e)except ValueError as e: print(e)
3.万能异常Exception
s1 = 'hello'try: int(s1)except Exception as e: print(e)
4.也可以在多分支后来一个Exception
s1 = 'hello'try: int(s1)except IndexError as e: print(e)except KeyError as e: print(e)except ValueError as e: print(e)except Exception as e: print(e)
5.异常的其他机构
s1 = 'hello'try: int(s1)except IndexError as e: print(e)except KeyError as e: print(e)except ValueError as e: print(e)#except Exception as e:# print(e)else: print('try内代码块没有异常则执行我')finally: print('无论异常与否,都会执行该模块,通常是进行清理工作')
6.主动触发异常
try: raise TypeError('类型错误')except Exception as e: print(e)
7.自定义异常
class EgonException(BaseException): def __init__(self,msg): self.msg=msg def __str__(self): return self.msgtry: raise EgonException('类型错误')except EgonException as e: print(e)
8.断言:assert 条件
assert 1 == 1 assert 1 == 2
9.总结try..except
- 把错误处理和真正的工作分开来
- 代码更易组织,更清晰,复杂的工作任务更容易实现;
- 毫无疑问,更安全了,不至于由于一些小的疏忽而使程序意外崩溃了;