练习40--模块、类和对象——面向对象的三大特性
面向对象的三大特性是指:封装、继承和多态。
一 封装
1 封装的基本概念
- 不同定义——不管什么形式,封装的都是数据和方法,无论哪种层面的封装,都要对外界提供好访问内部隐藏内容的接口。
- 代码封装,其实就是隐藏实现功能的具体代码,仅留给用户使用的接口,就好像使用计算机,用户只需要使用键盘、鼠标就可以实现一些功能,而根本不需要知道其内部是如何工作的
- 类的封装,即在设计类时,刻意地将一些属性和方法隐藏在类的内部,这样在使用此类时,将无法直接以“类对象.属性名”(或者“类对象.方法名(参数)”)的形式调用这些属性(或方法),而只能用未隐藏的类方法间接操作这些隐藏的属性和方法。
- 模块,可以理解为是对代码更高级的封装,即把能够实现某一特定功能的代码编写在同一个 .py 文件中,并将其作为一个独立的模块,这样既可以方便其它程序或脚本导入并使用,同时还能有效避免函数名和变量名发生冲突。
- 一般意义上的封装:我们一般讲的封装都是指类的封装
- 注意:封装绝不是将类中所有的方法都隐藏起来,一定要留一些像键盘、鼠标这样可供外界使用的类方法。
- 原因:封装不是单纯意义的隐藏
- 封装数据的主要原因是:保护隐私
- 封装方法的主要原因是:隔离复杂度
- 两个层面:
- 第一个层面的封装(什么都不用做):创建类和对象会分别创建二者的名称空间,我们只能用类名.或者obj.的方式去访问里面的名字,这本身就是一种封装。
- 注意:对于这一层面的封装(隐藏),类名.和实例名.就是访问隐藏属性的接口
- 第二个层面的封装:类中把某些属性和方法隐藏起来(或者说定义成私有的),只在类的内部使用、外部无法访问,或者留下少量接口(函数)供外部访问。
- 第一个层面的封装(什么都不用做):创建类和对象会分别创建二者的名称空间,我们只能用类名.或者obj.的方式去访问里面的名字,这本身就是一种封装。
2 类封装的好处
- 可维护性:封装机制保证了类内部数据结构的完整性,因为使用类的用户无法直接看到类中的数据结构,只能使用类允许公开的数据,很好地避免了外部对内部数据的影响,提高了程序的可维护性。
- 复杂度:对一个类实现良好的封装,用户只能借助暴露出来的类方法来访问数据,我们只需要在这些暴露的方法中加入适当的控制逻辑,即可轻松实现用户对类中属性或方法的不合理操作。
- 代码可移植性:对类进行良好的封装,还可以提高代码的复用性。
3 具有封装特性的结构:
- 诸多容器,例如列表、元组、字符串、字典等,它们都是对数据的封装;
- 函数是对 Python 代码的封装;
- 类是对方法和属性的封装,也可以说是对函数和数据的封装。
- 模块是对代码更高级的封装,将代码封装成一个文件,方便其它程序或脚本导入使用。
4 python类如何进行封装
- 原理:Python 类中的变量和函数,不是公有的(类似 public 属性),就是私有的(类似 private),因此可通过设置类中变量和函数的该属性来实现类的封装
- 区别:
- public:公有属性的类变量和类函数,在类的外部、类内部以及子类(后续讲继承特性时会做详细介绍)中,都可以正常访问;
- private:私有属性的类变量和类函数,只能在本类内部使用,类的外部以及子类都无法使用。
- 方法:
- 名称前没有下划线:默认这些python类中的变量和方法都是公有(public)的,可以通过类对象正常访问;
- 名称以双下划线“__”开头:表示这些python类中的变量和方法都是私有(private)的,只能在类内部使用,在类外部不能通过类对象进行访问;
- 类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式:
- 类中定义的__x只能在内部使用,如self.__x,引用的就是变形的结果。
- 这种变形其实正是针对外部的变形,在外部是无法通过__x这个名字访问到的。
- 在子类定义的__x不会覆盖在父类定义的__x,因为子类中变形成了:_子类名__x,而父类中变形成了:_父类名__x,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的。
- 注意:对于这一层面的封装(隐藏),我们需要在类中定义一个函数(接口函数)在它内部访问被隐藏的属性,然后外部就可以使用了。
- 这种变形需要注意的问题是:
- 这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如a._A__N
- 变形的过程只在类的定义是发生一次,在定义后的赋值操作,不会变形
- 类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式:
- 名称以单下划线“_”开头的类属性和类方法:通常将他们看作私有(private)的,但是它们可以通过类对象正常访问;
- 名称前后分别以双下划线开头和结尾:这些类方法都是python内部定义的,用于python内部调用,我们自己定义类属性或者类方法时,禁止使用这种形式。
5 调用被封装的内容(第一个层面的封装)
- 通过对象直接调用
-
1 class Foo: 2 3 def __init__(self, name, age): 4 self.name = name 5 self.age = age 6 7 obj1 = Foo('wupeiqi', 18) 8 print obj1.name # 直接调用obj1对象的name属性 9 print obj1.age # 直接调用obj1对象的age属性 10 11 obj2 = Foo('alex', 73) 12 print obj2.name # 直接调用obj2对象的name属性 13 print obj2.age # 直接调用obj2对象的age属性
-
- 通过self间接调用
-
1 class Foo: 2 3 def __init__(self, name, age): 4 self.name = name 5 self.age = age 6 7 def detail(self): 8 print self.name 9 print self.age 10 11 obj1 = Foo('wupeiqi', 18) 12 obj1.detail() # Python默认会将obj1传给self参数,即:obj1.detail(obj1),所以,此时方法内部的 self = obj1,即:self.name 是 wupeiqi ;self.age 是 18 13 14 obj2 = Foo('alex', 73) 15 obj2.detail() # Python默认会将obj2传给self参数,即:obj1.detail(obj2),所以,此时方法内部的 self = obj2,即:self.name 是 alex ; self.age 是 78
-
二 继承
1 继承的基本概念
- 定义:继承是一种创建新类的方式,新建的类可以继承一个或多个父类(python支持多继承),父类又可称为基类或超类,新建的类称为派生类或子类。
- 分类:python中类的继承分为:单继承和多继承
- 语法:
1 class 类名(父类1, 父类2, ...): 2 #类定义部分
- 特点:
- 如果该类没有显式指定继承自哪个类,则默认继承 object 类(object 类是 Python 中所有类的父类/基类,即要么是直接父类,要么是间接父类)。
- Python 的继承是多继承机制(和 C++ 一样),即一个子类可以同时拥有多个直接父类。
- 查看:
- 类名.__base__:只查看从左到右继承的第一个子类
- 类名.__bases__:查看所有继承的父类
- 经典类与新式类:
- 只有在python2中才分新式类和经典类,python3中统一都是新式类
- 在python2中,没有显式的继承object类的类,以及该类的子类,都是经典类
- 在python2中,显式地声明继承object的类,以及该类的子类,都是新式类
- 在python3中,无论是否继承object,都默认继承object,即python3中所有类均为新式类
2 继承与抽象(先抽象再继承)
- 抽象:
- 即抽取类似或者说比较像的部分。
- 抽象最主要的作用是划分类别(可以隔离关注点,降低复杂度)
- 抽象只是分析和设计的过程中,一个动作或者说一种技巧,通过抽象可以得到类
- 继承:
- 是基于抽象的结果,通过编程语言去实现它,肯定是先经历抽象这个过程,才能通过继承的方式去表达出抽象的结构。
- 继承描述的是子类与父类之间的关系,是一种什么是什么的关系。要找出这种关系,必须先抽象再继承
3 继承与重用性
- 代码重用:通过继承的方式新建类B,让B继承A,B会‘遗传’A的所有属性(数据属性和函数属性),实现代码重用
- 开发过程中,不仅可以重用自己的类,也可以继承别人的,比如标准库,来定制新的数据类型,这样就是大大缩短了软件开发周期,对大型软件开发来说,意义重大.
4 派生
- 定义:它和继承是一个意思,只是观察角度不同而已。换句话说,继承是相对子类来说的,即子类继承自父类;而派生是相对于父类来说的,即父类派生出子类。
- python父类方法的重写:
- 适用情况:对于父类中的一些方法,子类对象不能直接使用时,必须将这些方法在子类中重新定义一遍,即重写
- 定义:重写,有时又称覆盖,是一个意思,指的是对类中已有方法的内部实现进行修改。
- 举例:
1 class Bird: 2 #鸟有翅膀 3 def isWing(self): 4 print("鸟有翅膀") 5 #鸟会飞 6 def fly(self): 7 print("鸟会飞") 8 class Ostrich(Bird): 9 # 重写Bird类的fly()方法 10 def fly(self): 11 print("鸵鸟不会飞") 12 13 # 创建Ostrich对象 14 ostrich = Ostrich() 15 #调用 Ostrich 类中重写的 fly() 类方法 16 ostrich.fly()
- 调用重写之后的方法:子类对象名.重写后的方法,例如"ostrich.fly()"语句
- 调用被重写方法:父类名.被重写的方法名(子类对象名)
-
1 # 创建Ostrich对象 2 ostrich = Ostrich() 3 #调用 Bird 类中的 fly() 方法 4 Bird.fly(ostrich)
- Python 中的类可以看做是一个独立空间,而类方法其实就是出于该空间中的一个函数。
- 而如果想要全局空间中,调用类空间中的函数,只需要在调用该函数时备注类名即可。
- 使用类名调用其类方法,Python 不会为该方法的第一个 self 参数自定绑定值,因此采用这种调用方法,需要手动为 self 参数赋值。
-
5 关于python的多继承
- 大部分面向对象的编程语言,都只支持单继承,即子类有且只能有一个父类。而Python 却支持多继承(C++也支持多继承)
- 和单继承相比,多继承容易让代码逻辑复杂、思路混乱,一般较少使用
- 使用多继承经常需要面临的问题是,多个父类中包含同名的类方法
- 对于这种情况,Python 的处置措施是:根据子类继承多个父类时这些父类的前后次序决定,即排在前面父类中的类方法会覆盖排在后面父类中的同名类方法。
- 代码:
-
1 class People: 2 def __init__(self): 3 self.name = People 4 def say(self): 5 print("People类",self.name) 6 7 class Animal: 8 def __init__(self): 9 self.name = Animal 10 def say(self): 11 print("Animal类",self.name) 12 #People中的 name 属性和 say() 会遮蔽 Animal 类中的 13 class Person(People, Animal): 14 pass 15 16 zhangsan = Person() 17 zhangsan.name = "张三" 18 zhangsan.say()
- 运行结果:
-
1 People类 张三
6 继承的实现原理
- 继承顺序问题(继承了多个类):
- Python的类如果继承了多个类,那么其寻找方法的方式有两种,分别是:深度优先和广度优先
- 经典类和新式类的区别:
- 语法区分:是否继承了object类
- 多继承查找方式区分:
- 当类是经典类时,多继承情况下,会按照深度优先方式查找
-
1 class D: 2 3 def bar(self): 4 print 'D.bar' 5 6 7 class C(D): 8 9 def bar(self): 10 print 'C.bar' 11 12 13 class B(D): 14 15 def bar(self): 16 print 'B.bar' 17 18 19 class A(B, C): 20 21 def bar(self): 22 print 'A.bar' 23 24 a = A() 25 # 执行bar方法时 26 # 首先去A类中查找,如果A类中没有,则继续去B类中找,如果B类中么有,则继续去D类中找,如果D类中么有,则继续去C类中找,如果还是未找到,则报错 27 # 所以,查找顺序:A --> B --> D --> C 28 # 在上述查找bar方法的过程中,一旦找到,则寻找过程立即中断,便不会再继续找了 29 a.bar() 30 31 经典类多继承
- 首先去A类中查找,如果A类中没有,则继续去B类中找,如果B类中么有,则继续去D类中找,如果D类中么有,则继续去C类中找,如果还是未找到,则报错
-
- 当类是新式类时,多继承情况下,会按照广度优先方式查找
-
1 class D(object): 2 3 def bar(self): 4 print 'D.bar' 5 6 7 class C(D): 8 9 def bar(self): 10 print 'C.bar' 11 12 13 class B(D): 14 15 def bar(self): 16 print 'B.bar' 17 18 19 class A(B, C): 20 21 def bar(self): 22 print 'A.bar' 23 24 a = A() 25 # 执行bar方法时 26 # 首先去A类中查找,如果A类中没有,则继续去B类中找,如果B类中么有,则继续去C类中找,如果C类中么有,则继续去D类中找,如果还是未找到,则报错 27 # 所以,查找顺序:A --> B --> C --> D 28 # 在上述查找bar方法的过程中,一旦找到,则寻找过程立即中断,便不会再继续找了 29 a.bar() 30 31 新式类多继承
- 首先去A类中查找,如果A类中没有,则继续去B类中找,如果B类中么有,则继续去C类中找,如果C类中么有,则继续去D类中找,如果还是未找到,则报错
-
- 当类是经典类时,多继承情况下,会按照深度优先方式查找
- 语法区分:是否继承了object类
- 继承原理(python如何实现继承)
- 原理:
- python到底是如何实现继承的,对于你定义的每一个类,python会计算出一个方法解析顺序(MRO)列表,这个MRO列表就是一个简单的所有基类的线性顺序列表——F.mro() #等同于F.__mro__
- 为了实现继承,python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。
- MRO列表
- 构造:通过一个C3线性化算法来实现
- 实质:它实际上就是合并所有父类的MRO列表并遵循如下三条准则:
- 子类会先于父类被检查
- 多个父类会根据它们在列表中的顺序被检查
- 如果对下一个类存在两个合法的选择,选择第一个父类
- 原理:
7 子类中调用父类的方法
- 问题:
- 当一个子类继承多个类时,在创建子类的对象后,程序运行可能会出错;
- 因为不同父类的构造方法之间会被覆盖,导致有些父类的构造方法没有得到正确的参数;
- 为解决该问题,正确的做法是在子类中定义自己的构造方法(等同于重写第一个直接父类的构造方法),并且必须在该方法中调用第一个直接父类的构造方法
- 调用第一个父类的构造方法:
- 方法一:父类名.父类构造方法()
- 方法二:super()
- Python 2.x 语法:
1 super(Class, obj).__init__(self,...)
- python 3.x 语法:
1 super().__init__(self,...)
- Python 2.x 语法:
- 上述两种方式也可用于调用父类中的其它方法:
- 方法一:父类名.父类方法()
-
1 #_*_coding:utf-8_*_ 2 __author__ = 'Linhaifeng' 3 4 class Vehicle: #定义交通工具类 5 Country='China' 6 def __init__(self,name,speed,load,power): 7 self.name=name 8 self.speed=speed 9 self.load=load 10 self.power=power 11 12 def run(self): 13 print('开动啦...') 14 15 class Subway(Vehicle): #地铁 16 def __init__(self,name,speed,load,power,line): 17 Vehicle.__init__(self,name,speed,load,power) 18 self.line=line 19 20 def run(self): 21 print('地铁%s号线欢迎您' %self.line) 22 Vehicle.run(self) 23 24 line13=Subway('中国地铁','180m/s','1000人/箱','电',13) 25 line13.run()
-
- 方法二:super()
-
1 class Vehicle: #定义交通工具类 2 Country='China' 3 def __init__(self,name,speed,load,power): 4 self.name=name 5 self.speed=speed 6 self.load=load 7 self.power=power 8 9 def run(self): 10 print('开动啦...') 11 12 class Subway(Vehicle): #地铁 13 def __init__(self,name,speed,load,power,line): 14 #super(Subway,self) 就相当于实例本身 在python3中super()等同于super(Subway,self) 15 super().__init__(name,speed,load,power) 16 self.line=line 17 18 def run(self): 19 print('地铁%s号线欢迎您' %self.line) 20 super(Subway,self).run() 21 22 class Mobike(Vehicle):#摩拜单车 23 pass 24 25 line13=Subway('中国地铁','180m/s','1000人/箱','电',13) 26 line13.run()
-
- 方法一:父类名.父类方法()
三 多态
1 多态
- 定义:多态指的是一类事物有多种形态,(一个抽象类有多个子类,因而多态的概念依赖于继承)
- 序列类型有多种形态:字符串,列表,元组
- 动物有多种形态:人,狗,猪
- 满足条件:
- 继承:多态一定是发生在子类和父类之间;
- 重写:子类重写了父类的方法
- 代码举例:
-
1 #多态:同一种事物的多种形态,动物分为人类,猪类(在定义角度) 2 class Animal: 3 def run(self): 4 raise AttributeError('子类必须实现这个方法') 5 6 7 class People(Animal): 8 def run(self): 9 print('人正在走') 10 11 class Pig(Animal): 12 def run(self): 13 print('pig is walking') 14 15 16 class Dog(Animal): 17 def run(self): 18 print('dog is running') 19 20 peo1=People() 21 pig1=Pig() 22 d1=Dog() 23 24 peo1.run() 25 pig1.run() 26 d1.run()
-
2 多态性
- 定义:多态性是指具有不同功能的函数可以使用相同的函数名,这样就可以用一个函数名调用不同内容的函数。
- 在面向对象方法中一般是这样表述多态性:向不同的对象发送同一条消息,不同的对象在接收时会产生不同的行为(即方法)。
- 也就是说,每个对象可以用自己的方式去响应共同的消息。
- 所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。
- 代码举例:
-
1 #多态性:一种调用方式,不同的执行效果(多态性) 2 # 多态性依赖于:继承 3 ##多态性:定义统一的接口, 4 def func(obj): #obj这个参数没有类型限制,可以传入不同类型的值 5 obj.run() #调用的逻辑都一样,执行的结果却不一样 6 7 func(peo1) 8 func(pig1) 9 func(d1) 10 11 # peo1.run() 12 # pig1.run() 13 # d1.run()
- 综上可以说,多态性是一个接口(函数func),多种实现(如people.talk())
-
- 多态性的好处:
- 增加了程序的灵活性
- 以不变应万变,不论对象千变万化,使用者都是同一种形式去调用,如func(animal)
- 增加了程序的可扩展性
-
1 >>> class Cat(Animal): #属于动物的另外一种形态:猫 2 ... def talk(self): 3 ... print('say miao') 4 ... 5 >>> def func(animal): #对于使用者来说,自己的代码根本无需改动 6 ... animal.talk() 7 ... 8 >>> cat1=Cat() #实例出一只猫 9 >>> func(cat1) #甚至连调用方式也无需改变,就能调用猫的talk功能 10 say miao 11 12 ''' 13 这样我们新增了一个形态Cat,由Cat类产生的实例cat1,使用者可以在完全不需要修改自己代码的情况下。使用和人、狗、猪一样的方式调用cat1的talk方法,即func(cat1) 14 '''
通过继承animal类创建了一个新的类,使用者无需更改自己的代码,还是用func(animal)去调用
-
- 增加了程序的灵活性
- 总结:
- 多态:同一种事物的多种形态,动物分为人类,猪类(在定义角度) 多态性:一种调用方式,不同的执行效果(多态性)
3 鸭子模型
- 定义:Python 这种由多态衍生出的更灵活的编程机制,又称为“鸭子模型”或“鸭子类型”。
- 代码举例:
1 class WhoSay: 2 def say(self,who): 3 who.say() 4 class CLanguage: 5 def say(self): 6 print("调用的是 Clanguage 类的say方法") 7 8 class CPython(CLanguage): 9 def say(self): 10 print("调用的是 CPython 类的say方法") 11 12 class CLinux(CLanguage): 13 def say(self): 14 print("调用的是 CLinux 类的say方法") 15 a = WhoSay() 16 #调用 CLanguage 类的 say() 方法 17 a.say(CLanguage()) 18 #调用 CPython 类的 say() 方法 19 a.say(CPython()) 20 #调用 CLinux 类的 say() 方法 21 a.say(CLinux())
此程序中,通过给 WhoSay 类中的 say() 函数添加一个 who 参数,其内部利用传入的 who 调用 say() 方法。这意味着,当调用 WhoSay 类中的 say() 方法时,我们传给 who 参数的是哪个类的实例对象,它就会调用那个类中的 say() 方法。
四 参考内容
教学网站:http://c.biancheng.net/view/2288.html
博客园:https://www.cnblogs.com/linhaifeng/articles/7340153.html