前面学习了面向过程和面向对象的概念,又学习了Python中类和对象的相关知识,其中面向对象最大的特征是:封装、继承和多态。本文就分3大块内容,分别来详细学习面向对象中的三大特征。
封装
封装的概念来自电气电子元件,集成电路的一道工序,后来引用到面向对象的程序设计思想中。现实中处处可见封装的例子:比如你家的洗衣机。
把一台洗衣机看做洗衣机类的一个实例,洗衣机里有标准洗、速洗、精洗、桶干燥、洁桶等多种功能。作为用户不需要知道这些功能内部的具体实现,需要某项功能只要选择对应的功能键即可。这就是洗衣机封装了以上多种功能。
洗衣机-封装
相似地,在面向对象中,封装,说白了就是一个类中抽象出来的静态数据(即属性)以及该类的一些功能(即方法)。这就是前面学习的对象的属性和方法。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。这是前面学习的访问权限,在封装特性中,访问权限非常重要。
继承
在现实生活中一说到继承,你一定会想到"子承父业"这个词。因为这种父子关系,儿子"平白无故地"从父亲那里得到了家产基业等。
在面向对象的程序设计里,与此类似。假设A类继承自B类,则A类称为子类或派生类,B类称为父类,也叫基类、超类。那么A类就"子承父业",这里的"父业",就是父类中的属性和方法,如此,A类中自然就有了B类中的属性和方法。这种继承关系使得代码的复用性大大提高。
Python 同样支持类的继承,如果一种语言不支持继承,类就没有什么意义。不同的开发语言,继承的支持程度不一样。Objective-C、Java等是单继承,而Python、C++等是支持多继承的。
前面在学习python中类的定义时,当时这样解释说明:类的定义需要关键字class,class后面紧接着是类名,即ClassName,类名通常是大写开头的单词。紧接着是(object),表示该类是从哪个类继承下来的,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。表示类中的属性或方法。
既然,python是支持多继承的,那么类名后面的里,是可以有多个类的,即:。需要注意圆括号中父类的顺序,若是父类中有相同的方法名,而在子类使用时未指定,python从左至右搜索,即方法在子类中未找到时,从左到右查找基类中是否包含方法。
BaseClassName(基类名)这种写法必须与派生类定义在一个作用域内。通常,如果基类定义在另一个模块中时这一点非常有用:
我们假设定义了三个类:Animal动物类,Dog狗类,Cat猫类,模块结构分别如下:
单继承模块结构
Animal源码如下:
Dog狗类,继承自Animal类,源码:
Cat猫类继承自Animal类,源码如下:
测试模块,分别创建了一个猫类对象c、狗类对象d,分别调用c、d对象吃的方法,源码如下:
运行结果:
可以看到,都打印出了Animal类中方法中的内容。这样我们通过继承关系,让Dog\Cat两个类什么也不写就有了父类中的所有的属性和方法。
多继承
我们假设定义了4个类:Animal动物类,哺乳动物类Mammal、Dog狗类,Cat猫类,模块结构分别如下:
多继承模块结构
Animal动物类,哺乳动物类Mammal两个类,将作为Dog狗类,Cat猫类的父类。
Animal源码如下:
Mammal哺乳动物类,源码如下:
Dog狗类,继承自Animal类,源码如下:
Cat猫类继承自Animal类,源码如下:
测试模块,分别创建了一个猫类对象c、狗类对象d,分别调用c、d对象吃的方法,源码如下:
运行结果:
拓展
在Java中,使用关键字表示继承,Object是Java中所有类的基类,仅支持单继承。例如:
在Objective-C中,则使用表示继承。NSObject是OC中所有类的基类,且仅支持单继承。例如:
接下来要学习的面向对象的多态,正是在继承的基础上才有的特性:当子类和父类有了相同名称的方法时,就是多态了。
多态
多态特性涉及到父子类以及父类之间同名函数的调用顺序问题,所以前面提到了:一定要注意Python在自定义类时,类名后面的圆括号中父类的顺序。若是父类中有相同的方法名,而在子类使用时未指定,python从左至右搜索,即方法在子类中未找到时,从左到右查找基类中是否包含方法。
使用上面同样的类,我们定义了4个类:Animal动物类,哺乳动物类Mammal、Dog狗类,Cat猫类,模块结构分别如下:
多态-模块结构
Animal动物类,哺乳动物类Mammal两个类,将作为Dog狗类,Cat猫类的父类。
Animal源码如下:
Mammal哺乳动物类,源码如下:
Dog狗类,继承自Animal类,源码如下:
Cat猫类继承自Animal类,源码如下:
可以看到,Animal动物类,哺乳动物类Mammal两个类,作为Dog狗类,Cat猫类的父类,并且每个类中都有同名不同实现的方法:。
此时再在测试模块,分别创建了一个猫类对象c、狗类对象d,分别调用c、d对象吃的方法,源码如下:
运行结果如下:
子类的方法覆盖了父类中同名的方法,优先调用子类中同名的方法。
假设,现在需要调用父类中的方法,而不是调用子类中的方法,则可以这么做:
运行结果:
深入理解多态
要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样。比如上面的用到的对象:
isinstance(对象, 类型)
用来判断一个变量是否是该类型或其子类的实例。注意如果该对象是该类的子类的对象也返回True。
运行结果:
这和现实是匹配的:猫是猫类型,猫是动物。但是,反过来,动物是猫,就不行了。
运行结果:
知道了以上知识,我们再写一个方法:
运行结果:
至此发现什么没?如果没有,那么现在,如果我们再定义一个Wolf类型,也继承自Animal:
在测试模块,导入wolf模块,并创建一个wolf对象,源码如下:
运行结果:
你会发现,新增一个Animal的子类,不必对animal_eat()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
多态的好处
多态的好处就是,当我们在需要传入Dog、Cat、Wolf……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Wolf……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的方法,这就是多态的好处。
对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用方法,而具体调用的方法是作用在Animal、Dog、Cat还是Wolf对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保方法编写正确,不用管原来的代码是如何调用的。
拓展:静态语言 & 动态语言
对于静态语言来说,例如Java,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用方法。
对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个方法就可以了。例如,定义一个带有eat()方法的黑洞类:
运行结果:
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。
小结
本文详细学习了Python中面向对象的3大特性:封装、继承、多态,以及拓展了python中的鸭子类型。这三大特性是任何面向对象语言的基础知识,要深刻理解。
人人懂编程ID:pythonDNA
领取专属 10元无门槛券
私享最新 技术干货