
1.7 Python类
Python从设计之初就是一门面向对象的语言,正因为如此,在其中创建一个类和对象是很容易的。下面介绍Python的面向对象编程。
1.7.1 面向对象概述
面向对象(Object Oriented,OO)是一种软件开发方法,面向对象是一种软件对现实世界理解和抽象表示的方法,为计算机目前普遍采用的编程技术,是基于面向过程发展出来的。
面向对象的基本特征如图1-38所示。

图1-38 面向对象的基本特征
1.抽象
抽象是面向对象编程的第一步,完成从现实世界到计算机世界的转换,通过抽象的方法来理解这个现实世界,现实世界的一切物体都可以抽象成对象,一切软件系统都是由对象构成的。
在采用面向对象的方法处理数据的过程中,要把具体处理的对象使用程序语言描述出来,就是把处理的对象描述成一组对应的数据和方法,去除非本质、非特性、非有关的属性和方法,保留本质的、需要的和共性的属性和方法。
2.封装
封装是面向对象的最基本特点之一,也是面向对象的基础。对象可以没有继承、多态,但是不能没有封装,没有对象的封装就没有对象,数据封装就是指一组数据和与这组数据有关的操作集合组装在一起,形成一个能动的实体。封装给对象一个边界,使内部的数据信息尽量隐藏,只保留允许的对外的数据操作接口。
例如一个电视机,外部封装一个外壳,内部的元器件是看不到的,更是不允许直接插拔的,而是留下接口(包括电源接口、信号接口以及控制接口)来操作电视机。
3.继承
继承就是在类之间建立一种相交的关系,使得新定义的派生类可以继承已有的基类,而且可以在新定义的派生类中添加新的类成员或者替换已有的类成员,从而提高代码的复用性和扩充性。继承是面向对象最核心的特点,能够有效提高开发效率。
在面向对象程序设计中,当新定义一个类的时候,可以从某个或某些现有的类继承,新类称为派生类(Subclass),而被继承类称为基类、父类或超类。派生类拥有父类的属性和方法,并且可以拥有自己的属性和方法。
4.多态
多态字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。同一个东西表现出多种状态,在面向对象的描述中就是同一个函数接口,实现多种不同的表现方式。
在面向对象方法中一般这样表述多态性:向不同的对象发送同一条消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应同一消息。消息就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。
多态实现一般有覆盖和重载两种方式。
(1)覆盖:子类重新定义父类的虚函数。
(2)重载:允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
多态性增加了程序的灵活性、适应性,以不变应万变,不论何种变化,都可以使用同一种形式去调用。
1.7.2 类和对象
下面是面向对象的一些重要概念。
(1)类(Class):用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。
(2)类变量:类变量在整个实例化的对象中是公用的。类变量定义在类中,且在函数体之外。类变量通常不作为实例变量使用。
(3)数据成员:类变量或者实例变量用于处理类及其实例对象的相关数据。
(4)方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写。
(5)实例变量:定义在方法中的变量,只作用于当前实例的类。
(6)继承:即一个派生类(Derived Class)继承基类(Base Class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。
(7)实例化:创建一个类的实例,类的具体对象。
(8)方法:类中定义的函数。
(9)对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。
和其他编程语言相比,Python在尽可能不增加新的语法和语义的情况下加入了类机制。Python中的类提供了面向对象编程的所有基本功能:类的继承机制允许多个基类,派生类可以覆盖基类中的任何方法,方法中可以调用基类中的同名方法,对象可以包含任意数量和类型的数据。
1.7.3 面向对象程序设计方法
传统的结构化程序设计通过设计一系列的过程(即算法)来解决问题,面向过程的程序设计算法是第一位的,数据结构是第二位的。面向过程编程是一种以过程为中心的编程思想,分析出解决问题的步骤,然后用函数把这些步骤一步一步实现。
面向对象程序设计将数据放在第一位,然后再考虑操作数据的算法,是一种以事件或消息来驱动对象运行处理的程序设计技术。它具有封装性、继承性以及多态性。
面向对象程序设计一般遵循以下原则。
(1)单一职责原则:一个类一般负责一项职责,提高类的可读性,提高系统的可维护性,降低变更引起的风险,提高内聚性。
(2)里氏替换原则:超类存在的地方,子类是可以替换的。在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时确定其子类类型后,用子类对象来替换父类对象。
(3)接口隔离原则:应提供单一接口,不要建立庞大的接口,要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。专用的接口要比综合的接口更灵活,提高系统的灵活性和可维护性。
(4)依赖倒置原则:尽量依赖抽象实现,而非依赖具体实现,不能有循环依赖。采用依赖倒置原则可以减少类之间的耦合性,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和可维护性。
(5)开闭原则:面向扩展开放,面向修改关闭。
(6)迪米特法则:又叫最少知识原则,一个软件实体应当尽可能少地与其他实体发生相互作用。
(7)组合/聚合复用原则:尽量使用组合/聚合达到复用,尽量少用继承。
1.7.4 类的定义和使用
Python是面向对象的编程语言,支持面向对象的各种功能,下面介绍Python类的定义和使用方法。
1.类定义
语法格式如下。

类实例化后,可以使用其属性,实际上,创建一个类之后,可以通过类名访问其属性。
2.类对象
类对象支持两种操作:属性引用和实例化。属性引用和Python中所有的属性引用的标准语法一样:obj.name。类对象创建后,类命名空间中所有的命名都是有效属性名。
类定义简单实例如下。

以上创建了一个新的类实例,并将该对象赋给局部变量x,执行以上程序输出结果如下。

很多类都倾向于将对象创建为有初始状态。因此可能需要定义一个名为__init__()的特殊方法(构造方法),格式如下。

如果类定义了__init__()方法,类的实例化操作会自动调用__init__()方法。所以在下例中,可以这样创建一个新的实例。

当然,__init__()方法可以有参数,参数通过__init__()传递到类的实例化操作上,例如:

类的方法与普通的函数只有一个区别,它们必须有一个额外的第一个参数名称,按照惯例它的名称是self,例如:

以上实例执行结果如下。

从执行结果可以很明显地看出,self代表的是类的实例,代表当前对象的地址,而self.class则指向类。
self不是Python关键字,我们把它换成其他内容也是可以正常执行的,例如:

以上实例执行结果如下。


3.类的方法
在类内部,使用def关键字来定义一个方法,与一般函数定义不同,类方法必须包含参数self,且为第一个参数,self代表的是类的实例,定义方法如下。

执行以上程序输出结果为“lisi说:我10岁”。
4.继承
Python同样支持类的继承,派生类的定义方法如下。

需要注意圆括号中基类的顺序,若是基类中有相同的方法名,而在子类使用时未指定,Python将从左至右搜索,即方法在子类中未找到时,从左到右查找基类中是否包含方法。
BaseClassName(实例中的基类名)必须与派生类定义在一个作用域中。除了类,还可以用表达式,基类定义在另一个模块中时这一方法非常有用,格式如下。

例如:


执行以上程序输出结果为“ken说:我10岁了,我在读3年级”。
5.多继承
Python有限地支持多继承形式。多继承的类定义形式如下。

需要注意圆括号中父类的顺序,若是父类中有相同的方法名,而在子类使用时未指定,Python将从左至右搜索,即方法在子类中未找到时,从左到右查找父类中是否包含方法。
简单实例如下。


执行以上程序输出结果如下。

6.方法重写
如果父类方法的功能不能满足需求,可以在子类中重写父类的方法,实例如下。

执行以上程序输出结果为“调用子类方法”。
7.类属性与方法
1)类的私有属性
__private_attrs:两个下划线开头,声明该属性为私有,不能在类的外部被使用或直接访问。在类内部的方法中使用时格式为self.__private_attrs。
2)类的方法
在类的内部,使用def关键字来定义一个方法,与一般函数定义不同,类方法必须包含参数self,且为第一个参数,self代表的是类的实例。
self的名字并不是不可改变的,也可以使用this,但是最好还是按照约定使用self。
3)类的私有方法
__private_method:两个下划线开头,声明该方法为私有方法,只能在类的内部调用,不能在类地外部调用。
类的私有属性实例如下。

执行以上程序输出结果如下。


类的私有方法实例如下。

以上实例执行结果如下。

8.类的专有方法
类的专有方法说明如下。
__init__:构造函数,在生成对象时调用。
__del__:析构函数,释放对象时使用。
__repr__:打印、转换。
__setitem__:按照索引赋值。
__getitem__:按照索引获取值。
__len__:获得长度。
__cmp__:比较运算。
__call__:函数调用。
__add__:加运算。
__sub__:减运算。
__mul__:乘运算。
__div__:除运算。
__mod__:求余运算。
__pow__:乘方。
9.运算符重载
Python支持运算符重载,可以对类的专有方法进行重载,实例如下。

以上代码执行结果如下。

1.7.5 多线程
多线程类似于同时执行多个不同程序,多线程运行有如下优点。
(1)使用线程可以把程序中占据长时间的任务放到后台去处理。
(2)可以改观用户界面,比如用户单击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
(3)程序的运行速度可能加快。
(4)在一些等待的任务实现上,线程比较实用,如用户输入、文件读写和网络收发数据等,在这种情况下我们可以释放内存占用等资源。
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
每个线程都有自己的一组CPU寄存器,称为线程的上下文,该上下文反映了上次运行该线程的CPU寄存器的状态。指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器。
线程可以分为如下两种。(1)内核线程:由操作系统内核创建和撤销。(2)用户线程:不需要内核支持而在用户程序中实现的线程。
Python线程中常用的两个模块为_thread和threading(推荐使用)。
_thread模块已被废弃。用户可以使用threading模块代替。所以,在Python3中不能再使用_thread模块。考虑到兼容性,Python3将thread重命名为_thread。
Python中使用线程有两种方式:用函数或者类来包装线程对象。
(1)函数式:调用_thread模块中的start_new_thread()函数来产生新线程,语法如下。

其中,function为线程函数;args为传递给线程函数的参数,必须是个tuple类型;kwargs为可选参数。
线程简单实例如下。

执行以上程序输出结果如下。


按下Ctrl+C键退出。
(2)通过调用threading模块继承“threading.Thread”类来包装一个线程对象。
实例如下。

执行以上程序输出结果如下。

其实thread和threading的模块中还包含了锁、定时器、获得激活线程列表等关于多线程编程的功能。