1.6 面向对象编程
Python从被设计之初就是一门面向对象的语言,正因为如此,在Python中创建一个类和对象是很容易的。本节从面向对象的概念说起,带领读者掌握用Python网络框架进行开发所必需的面向对象编程知识。
1.6.1 什么是面向对象
面向对象程序设计(Object Oriented Programming,OOP)是一种程序设计范型,也是一种程序开发方法。对象指的是类的实例,类是创建对象的模板,一个类可以创建多个对象,每个对象都是类类型的一个变量;创建对象的过程也叫作类的实例化。面向对象程序设计将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。面向对象编程中的主要概念如下。
· 类(class):定义了一件事物的抽象特点。通常来说,类定义了事物的属性和它可以做到的行为。举例来说,设计一个电子画板程序中的“Figure”类,它包含二维图形的一切基本特征,即所有二维图形共有的特征或行为,例如它的制作者、颜色、是否实心等。类可以为程序提供模板和结构。一个类中可以有成员函数和成员变量。在面向对象的术语中,成员函数被称为方法;成员变量被称为属性。
· 对象(object):是类的实例。例如,“Figure”类定义了图形的概念,而在电子画板程序中画出一个图形时,则是建立了一个Figure类的实例,即对象。当一个类被实例化时,它的属性就有了具体的值,比如该图形有了作者、某种具体的颜色。每个类可以有若干个被实例化的对象。在操作系统中,系统给对象分配内存空间,而不会给类分配内存空间。
· 继承(inheritance):是指通过一个已有的类(父类)定义另外一个类(子类),子类共享父类开放的属性和方法。子类的对象不仅是子类的一个实例,而且是其父类的一个实例。比如,可以从图形父类Figure继承并定义一个方形子类Rectangle,它具备Figure类的一切特征,并具备自己的独有特征,比如长度、宽度。在画板上画出一个方形实例时,它就是一个Rectangle,也是一个Figure。
· 封装性(Encapsulation):是指类在定义时可以将不能或不需要其他类知道的成员定义成私有成员,而只公开其他类需要使用的成员,以达到信息隐蔽和简化的作用。在画板程序的Figure类中,可以定义Move方法为公开成员,而Move方法需要调用的其他成员(clear、paintline、paint_color等)可以定义为私有成员。
· 多态性(Polymorphism):是指同一方法作用于不同的对象,可以有不同的解释,产生不同的执行结果。在具体实现方式上,多态性是允许开发者将父对象的变量设置为对子对象的引用,赋值之后,父对象变量就可以根据当前的赋值给它的子对象的特性以不同的方式运作。比如设计两个Figure的子类圆形Circle和方形Rectangle,两个子类的绘制(Paint)实现方法肯定不相同,因此当用父对象变量分别引用并调用两个子对象的Paint方法时会产生不同的效果。
随着面向对象编程的普及,面向对象设计(Object Oriented Design,OOD)也日臻成熟,形成了以UML(Unified Modeling Language)为代表的标准建模语言。UML是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供了模型化和可视化支持,包括由需求分析到规格,再到构造和配置的所有阶段。
1.6.2 类和对象
类和对象是面向对象编程的基础,在本节我们学习类的基本定义、对象的使用方法等。
1.基本使用
在Python中通过关键字class实现类的定义,其语法为:
在块block_class中写入类的成员变量及函数。如下是一个类MyClass的定义:
类定义代码的解析如下。
· 类名为MyClass。
· 该类中定义了一个成员变量message,并对其赋了初始值。
· 类中定义了成员函数show(self),注意类中的成员函数必须要带参数self。
· 参数self是对象本身的引用,在成员函数体中可以引用self参数获得对象的信息。
使用该类的代码如下:
通过在类名后面加小括号可以直接实例化类来获得对象变量,使用对象变量可以访问类的成员函数及成员变量。
注意:Python中直接在类作用域中定义的成员变量相当于C、C++中的静态成员变量,即可以通过类名访问,也可以通过对象访问。因此,类和所有该类的对象共享同一个成员变量。
2.构造函数
构造函数是一种特殊的类成员方法,主要用来在创建对象时初始化对象,即为对象成员变量赋初始值。Python中的类构造函数用__init__命名,为MyClass添加构造函数方法,并实例化一个对象。
【示例1-34】构造函数的示例代码如下:
构造函数在MyClass被实例化时被Python解释器自动调用,代码的输出如下:
【示例1-35】如果需要用多种方式构造对象,则可通过默认参数的方式实现:
在上述代码中定义了3个构造函数,分别接收0、1、2个构造参数,之后分别通过不同的构造参数构造实例。代码的运行结果如下:
注意:在构造函数中不能有返回值。
如果开发者试图调用未被定义过的构造函数,则会在运行时导致异常,比如:
将会导致异常:
技巧:Python中不能定义多个构造函数,但可以通过为命名参数提供默认值的方式达到用多种方式构造对象的目的。
3.析构函数
析构函数是构造函数的反向函数,在销毁(释放)对象时将调用它们。析构函数往往用来做“清理善后” 的工作,例如数据库链接对象可以在析构函数中释放对数据库资源的占用。Python中为类定义析构函数的方法是在类中定义一个名为__del__的没有返回值和参数的函数。
与Java类似,Python解释器的堆中储存着正在运行的应用程序所建立的所有对象,但是它们不需要程序代码来显式地释放,因为Python解释器会自动跟踪它们的引用计数,并自动销毁(同时调用析构函数)已经没有被任何变量引用的对象。在这种场景中,开发者并不知道对象的析构函数何时会被调用。同时,Python提供了显式销毁对象的方法:使用del关键字。
【示例1-36】为MyClass类添加析构函数的代码如下:
用del释放对象时析构函数会自动被调用,代码的运行结果如下:
4.实例成员变量
在之前的例子中,MyClass类中的成员变量message是类成员变量,即MyClass类和所有MyClass对象共享该成员变量。那么如何定义属于每个对象自己的成员变量呢?答案是在构造函数中定义self引用中的变量,这样的成员变量在Python中叫作实例成员变量。
【示例1-37】实例成员变量的示例如下:
本例在构造函数__init__中定义了两个实例成员变量:self.name和self.color;在MyClass的成员函数(如本例中的show()函数和析构函数)中可以直接使用这两个成员变量,通过实例名也可以访问到实例成员变量(本例中的 inst2.color、inst3.name)。代码的运行结果如下:
5.静态函数和类函数
到目前为止,读者在本书中接触到的类成员函数均与实例绑定,即只能通过对象访问而不能通过类名访问。Python中支持两种基于类名访问成员的函数:静态函数和类函数,它们的不同点是类函数有一个隐性参数 cls 可以用来获取类信息,而静态函数没有该参数。静态函数使用装饰器@staticmethod定义,类函数使用装饰器@classmethod定义。
【示例1-38】静态函数和类函数的代码示例如下:
在该段代码中定义了静态函数printMessage(),在其中可以访问类成员变量MyClass.message,可以通过类名对它进行调用;代码中还定义了类方法createObj(),类方法定义中的第1个参数必须为隐性参数cls,在类方法createObj()中可以通过隐性参数cls替代类名本身,本例中的createObj建立并返回了一个MyClass实例。代码的运行结果如下:
6.私有成员
封装性是面向对象编程的重要特点,Python也提供了将不希望外部看到的成员隐藏起来的私有成员机制。但不像大多数编程语言用Public、Private关键字表达可见范围的方法,Python使用指定变量名格式的方法定义私有成员,即所有以双下画线“__”开始命名的成员都为私有成员。
【示例1-39】代码示例如下:
本例中的构造函数将实例成员参数设置为私有形式,不影响在类本身的其他成员函数中访问这些变量(本例在析构函数中访问了__name属性)。但是类之外的代码无法访问私有成员,比如如下代码在运行中将产生AttributeError异常:
1.6.3 继承
类之间的继承是面向对象设计的重要方法,通过继承可以达到简化代码和优化设计模式的目的。Python类在定义时可以在小括号中指定基类,所有Python类都是object类型的子类,语法如下:
【示例1-40】子类除了具备自己block_class中定义的特性,还从父类中继承了父类的非私有特性,举例如下:
解析如下。
· 定义了一个基类Base,基类继承自object,并且定义了构造函数、析构函数、成员函数move()。
· 定义了子类SubA,继承自Base类,定义、重载了自己的构造函数、成员函数move()。
· 定义了子类SubB,继承自Base类,定义、重载了自己的析构函数。析构函数中用super关键字调用基类的析构函数__del__()。
· 完成类的定义后,分别实例化了两个子类的对象,并调用了它们的move方法和析构函数。
技巧:在子类成员函数中用super关键字可以访问父类成员,其引用方法为super (Sub ClassName, self)。
代码的执行结果如下:
对结果的解析如下。
· instA调用了子类SubA自己的构造函数和move()方法,但因为SubA没有重载析构函数,所以对象销毁时系统调用了基类Base的析构函数。
· 子类SubB只重载了析构函数,所以instB调用了基类的构造函数和move()方法,在对象销毁时调用了SubB自己的析构函数。
· move()方法在被instA和instB调用时分别展现了不同的行为,这种现象是多态。
技巧:在子类的析构函数中调用基类的析构函数是一种最佳实践,不这样做可能导致父类资源不能如期被释放。
【示例1-41】Python中允许类的多继承,也就是一个子类可以有多个基类,举例如下:
该段代码中定义了两个基类,两个基类中都定义了move()方法。BaseC继承自BaseA并且重载了move()函数。Sub继承自BaseC和BaseB,并且没有定义自己的成员。调用子类对象的move()方法的结果如下:
此处读者需要体会的是:当子类继承了多个父类,并且调用一个在几个父类中共有的成员函数时,Python解释器会选择距离子类最近的一个基类的成员方法。本例中Sub继承自BaseC和BaseB,所以move()方法的搜索顺序是:Sub、BaseC、BaseA、BaseB。
注意:设计多父类的继承关系时,要尽量避免多个父类中出现同名成员。如果不可避免,则应当留意子类定义中引用父类的顺序。