第5章 面向对象编程 4min 面向对象是一个相对较抽象的概念,初学者在刚接触时可能会比较难以适应和接受,为此本章尽可能地用生活中常见的例子来对抽象的理论进行类比和剖析,让读者能快速接受这些概念,并学会用面向对象的思想进行编程。本章首先对面向对象的相关理论概念进行介绍和整理,接着对比面向过程编程与面向对象编程的异同点,再介绍面向对象编程的语法格式及编程方式,通过两道面向对象的综合示例编程题展示面向对象编程的思维方式及编程技巧,最后介绍类的继承及Python中的异常处理机制。 5.1面向对象 在3.3节介绍过Python对象的相关概念,读者明白了Python支持的所有数据类型(数字、字符串、列表等)都是对象,数据类型对象中有属性(数据、变量)和方法(函数),在前面的章节中也通过各种各样的编程实例,展示过数据类型对象属性、函数的相关使用方法和技巧。既然Python所有的数据类型都是对象,这些数据类型对象都是Python已经定义好的,那么用户可以自定义属于自己的对象吗?当然可以,本章主要介绍如何自定义对象及如何使用面向对象的思维进行编程。 5.1.1类和对象的概念 根据前面章节的知识,我们已经能够创建各种数据类型的对象,例如能够创建多个字符串对象: "abc""xyz""lmn"等,观察这些字符串对象,可以发现这些字符串对象都具有相同的属性和方法,如果把具有相同属性和方法的字符串对象划分成同一个类别,则这个类别就可以称为字符串类。也就是说,字符串对象"abc""xyz""lmn"等都属于字符串类。 由上述可知,类是具有相同属性和方法的对象集合,例如字符串类是字符串对象"abc""xyz""lmn"等的集合,类将对象的共有特征抽象出来并封装。3.3节从代码的层面上介绍过Python对象的概念: 对象是将属性(数据、变量)和功能(方法、函数)封装在一起后产生的具有特定接口的内存块。从面向对象思维的层面上讲,对象是类的实例,即字符串对象"abc""xyz""lmn"等也称为字符串类的实例,创建一个对象的过程称为类的实例化,例如创建字符串对象"abc"的过程可以称为字符串类的实例化。 打个简单的比方,人共同具有的特征有鼻子、眼睛、嘴巴等,可以把鼻子、眼睛、嘴巴等看作人的属性,人都可以通过鼻子呼吸、通过眼睛观察事物、通过嘴巴吃饭等,可以把呼吸、观察事物、吃饭等人共同具有的行为看作人的方法,将人的属性(鼻子、眼睛、嘴巴等)和人的方法(呼吸、观察事物、吃饭等)封装在一起就组成了人类。人类的实例就是指每个具体的人,如小明、小李、小芳等,小明、小李、小芳就是人类实例化产生的对象,人类对象具有人类的所有属性和方法。 将上述介绍的相关重要概念整理后如表51所示。 表51面向对象相关重要概念整理 序号 概念 详 细 说 明 1 类 具有相同属性和方法的对象集合 2 对象 类的实例,具有类的所有属性和方法 3 类的实例化 创建一个对象(类的实例) 4 封装 将属性和方法组合到一起 5 类的属性 类的成员变量 6 类的方法 类的成员函数 5.1.2面向过程编程与面向对象编程比较 面向过程编程首先思考的是完成任务所需要的步骤顺序,然后一步一步实现这些步骤(步骤可以用函数实现),再一步一步按顺序调用这些实现步骤的函数及其他一些完成任务所需要的代码。面向对象编程首先思考的是如何将任务中具有相同特征的物体抽象成类进行封装,通过类的实例化产生对象,通过对象的属性和方法帮助完成任务编程。面向过程编程和面向对象编程的区别在于设计思想和思考问题解决方案的方式不同,在Python中两种编程方式通常被结合使用。面向过程编程和面向对象编程比较如表52所示。 表52面向过程编程和面向对象编程比较 序号 概念 详 细 说 明 1编程思想 面向过程编程强调程序的执行流程 面向对象编程构造类和对象完成编程任务 2优点 面向过程编程程序开销小,运行效率高,程序结构清晰,可读性强 面向对象编程易维护、易扩展,编程思想更贴近人们的现实生活,大大降低编程重复的工作量,使得程序更加灵活 3缺点 面向过程编程不易维护、不易扩展,编程的工作量大,不够灵活 面向对象编程由于要构造类和类实例,程序开销更大,占用内存空间大,程序的运行性能更低一些,但随着硬件技术的发展,一定程度上弥补了占用程序空间大的不足 5.2类、对象的创建和使用 5.2.1类的定义及实例化 1. 类的语法及对象的创建 类是具有相同属性和方法的对象集合,类的实例化结果产生对象,因此要想创建自定义的对象进行编程使用,必须先进行类的定义。在Python中通过class语句来定义一个类,语法格式如下: class类名: 类体 在类的语法格式中,类名后要跟一个冒号,类体中可以定义抽象出的类属性和类方法。以定义一个动物园类为例,假设动物园都有大象、猴子、长颈鹿这3种动物,动物园具有售卖门票、带领游客游园观赏的功能。要定义这样一个动物园类,就要抽象出动物园的共同特征(属性和方法),由于动物园都有大象、猴子、长颈鹿这3种动物,因此可以把这3种动物抽象成类的属性,又由于动物园具有售卖门票、带领游客游园观赏的功能,因此可以把这2种功能抽象成类的方法,且动物园能够售卖门票,说明门票也是动物园的共同属性,也应该将门票抽象成类的属性。定义动物园类的示例代码如下: //Chapter5/5.2.1_test1.py 输入: class Zoo: #定义动物园类,类名为Zoo,类名首字母一般大写 elephant="大象" #定义类属性: 大象 monkey="猴子" #定义类属性: 猴子 giraffe="长颈鹿" #定义类属性: 长颈鹿 tickets=500 #定义类属性: 门票,初始化为500张 def __init__(self): #初始化函数,该函数含义及使用将在后面的内容中介绍 pass def sellTicket(self): #定义类方法: 售卖门票,参数self的含义将在后面 #的内容中介绍 Zoo.tickets=Zoo.tickets-1 #调用类的属性变量"类名.变量名" return Zoo.tickets #返回门票数量 def watch(self): #定义类方法: 带领游客游园观赏 print("正在带游客游园观赏") print(Zoo) 输出: 无论是要在类的外部还是类的内部中使用定义的类属性变量,如上述代码中的elephant、tickets等,都需要通过“类名.变量名”的方式才能进行调用,调用方式如Zoo.elephant、Zoo.tickets。因为上述代码中的__init__(self)函数及所有函数中的参数self具有比较丰富的含义,所以将在后面的内容中进行更详细的介绍,读者在此处先当其都不存在,但也需先记住: 若想在对象中能够调用类中的函数,则该函数的第1个参数应定义为self(当然也可以是别的名称,但是习惯上人们将这个参数的名称称为self),否则会报错。 动物园类定义好后,可以通过“类名()”的语法格式进行类的实例化(创建一个对象),示例代码如下: //Chapter5/5.2.1_test2.py 输入: class Zoo: #定义动物园类,类名为Zoo elephant="大象" monkey="猴子" giraffe="长颈鹿" tickets=500 def __init__(self): pass def sellTicket(self): Zoo.tickets=Zoo.tickets-1 return Zoo.tickets def watch(self): print("正在带游客游园观赏") zoo=Zoo() #通过"类名()"进行动物园类的实例化,zoo就是动物园类的一个对象 实例化好后,跟数据类型对象的函数/属性使用方法一样,通过“对象名.函数/属性”的语法格式就能够使用定义好的类属性和类方法,示例代码如下: //Chapter5/5.2.1_test3.py 输入: class Zoo: #定义动物园类,类名为Zoo,类名首字母一般大写 elephant="大象" #定义类属性: 大象 monkey="猴子" #定义类属性: 猴子 giraffe="长颈鹿" #定义类属性: 长颈鹿 tickets=500 #定义类属性: 门票,初始化为500张 def __init__(self): #初始化函数,该函数含义及使用将在后面的内容中介绍 pass def sellTicket(self): #定义类方法: 售卖门票,参数self的含义将在后面的内容中介绍 Zoo.tickets=Zoo.tickets-1 #调用类的属性变量"类名.变量名" return Zoo.tickets #返回门票数量 def watch(self): #定义类方法: 带领游客游园观赏 print("正在带游客游园观赏") zoo=Zoo() #进行动物园类的实例化,zoo就是动物园类的一个对象 tickets=zoo.tickets #通过"对象名.属性"的语法格式调用门票属性 print(f"动物园开始时的门票有{tickets}张") tickets=zoo.sellTicket() #通过"对象名.函数"的语法格式调用售卖门票功能 print(f"售卖出1张门票后还剩下{tickets}张") species=[zoo.elephant,zoo.monkey,zoo.giraffe] #动物园中的动物种类 print(f"动物园中的动物各类有",species) 输出: 动物园开始时的门票有500张 售卖出1张门票后还剩下499张 动物园中的动物各类有 ['大象', '猴子', '长颈鹿'] 注意: 对同一个问题任务,不同的编程者可能抽象出的属性和方法存在一定的差异,读者不用太过纠结,能够完成任务即可。 2. 参数self在函数中的作用 上述代码Zoo类中的所有函数,第1个参数位置都定义了self这个参数,参数self在定义类的方法时是必须作为第1个参数而存在的,self代表的是类的实例,而不是类,在调用类的方法时不必为它传入任何值,如上述代码zoo对象调用Zoo类中的sellTicket(self)售卖门票方法,就没有为参数self传入对应的值。 既然不必为它传入任何值,又为什么要定义这样一个参数呢?前面介绍函数的时候介绍过,如果定义了参数且参数没有默认值,则在调用时不传入该参数是会报错的,那为什么这里不用为参数self传入任何值呢?到底该怎样来理解参数self呢? 先看如下示例代码: //Chapter5/5.2.1_test4.py 输入: class Zoo: #定义动物园类 tickets=500 #定义类属性: 门票,初始化为500张 def sellTicket(self): #在sellTicket(self)函数第1个位置定义参数self print(f"卖出门票1张") zoo=Zoo() #实例化一个zoo对象 zoo.sellTicket() #调用售卖门票功能 输出: 卖出门票1张 上述代码中定义了一个Zoo类的zoo对象,zoo对象可以直接调用Zoo类中定义了参数self的sellTicket(self)方法,那么如果sellTicket(self)方法中没有定义参数self,则上述代码的执行结果还会一样吗?将sellTicket(self)函数中的self删掉,重新定义成sellTicket()后,代码修改后运行结果如下: //Chapter5/5.2.1_test5.py 输入: class Zoo: tickets=500 def sellTicket(): #删掉了参数self print(f"卖出门票1张") zoo=Zoo() zoo.sellTicket() #删掉参数self后通过zoo.sellTicket()调用售卖门票功能 输出: Traceback (most recent call last): File "D:/myPython/test.py", line 6, in zoo.sellTicket() TypeError: sellTicket() takes 0 positional arguments but 1 was given 由上述代码可知,当把sellTicket(self)函数中的参数self删掉后,再通过zoo对象调用Zoo类中的sellTicket()方法时,运行结果报错了,也就是说删掉参数self后,zoo对象无法调用和识别Zoo类中未定义参数self的方法,为什么呢? 实际上,定义好Zoo类后,通过Zoo类创建zoo对象,如果Zoo类中的sellTicket(self)函数定义了参数self,则zoo对象调用sellTicket(self)函数的执行过程zoo.sellTicket()被转换成Zoo.sellTicket(zoo)来执行,也就是将zoo对象作为参数值传入了sellTicket(self)函数中,zoo对象与参数self相互对应。 前面我们讲过,self代表的是类实例,而不是类,不用我们手动为self传入值,就是因为Python自动地将zoo对象作为参数值传入类函数中的参数self,因此这里的self代表的是像zoo这样的类实例对象。假如在类函数的参数中没有定义self,因为Python执行zoo.sellTicket()需要被转换成Zoo.sellTicket(zoo)来执行,如果sellTicket()函数中没有定义参数self则无法接收zoo对象作为参数值的传入,进而会导致参数不匹配的错误。所以读者需记住,在类中定义函数,一定要将self作为函数的第1个参数进行定义,否则在类实例对象中无法调用该函数。 我们也可以来看一下zoo.sellTicket()与Zoo.sellTicket(zoo)的执行结果,示例代码如下: //Chapter5/5.2.1_test6.py 输入: class Zoo: tickets=500 def sellTicket(self): #带参数self print(f"卖出门票1张") zoo=Zoo() #实例化对象zoo print("下面是zoo.sellTicket()的执行结果: ") zoo.sellTicket() print("下面是Zoo.sellTicket(zoo)的执行结果: ") Zoo.sellTicket(zoo) 输出: 下面是zoo.sellTicket()的执行结果: 卖出门票1张 下面是Zoo.sellTicket(zoo)的执行结果: 卖出门票1张 上述代码结果表明,zoo.sellTicket()与Zoo.sellTicket(zoo)的执行效果等同。 再深入看一下类实例对象调用类中带参数self的函数执行过程,定义好Zoo类后,通过Zoo类创建了3个zoo对象: zoo_1、zoo_2、zoo_3,Zoo类中定义有带参数self的sellTicket(self)函数,则zoo_1.sellTicket()的执行过程被转换为Zoo.sellTicket(zoo_1)来执行,zoo_2.sellTicket()的执行过程被转换为Zoo.sellTicket(zoo_2)来执行,zoo_3.sellTicket()的执行过程被转换为Zoo.sellTicket(zoo_3)来执行。 上述过程表明,每创建好一个新对象,当新对象要调用类中的函数时,Python会自动地将该对象作为参数传入函数的self中,以转换成“类名.函数名(对象名)”的方式执行,这样的机制也使多个对象之间相互独立,更加灵活,不会相互产生影响。 注意: 必须明确的是,self这个名称不是固定不变的,只要这个参数对应的位置是第1位,实际上可以由用户定义各种符合Python命名规则的名字,但人们约定俗成地将这个参数名称称为self,代表类实例。 3. self的实例变量及__init__()函数 在上述的内容中,我们知道了参数self代表的是类实例化产生的对象,既然参数self代表对象,而对象又有变量(属性)和方法,也就说明self同样具有变量和方法。实际上,我们已经认识过了self中的方法,在类的内部,凡是带有参数self的函数都属于self的方法,可以通过“self.函数”直接调用,在类的外部,带有参数self的函数能被类的实例对象所调用,但如果类内部的函数不带有参数self则该函数不属于self的方法,在类的外部不能被类的实例对象所调用。self实例在类内部和外部调用带参数self的方法使用示例代码如下: //Chapter5/5.2.1_test7.py 输入: class Zoo: def __init__(self): pass def sellTicket1(self): #定义一个带参数self的函数 print(f"门票一次性卖出50张") def sellTicket2(self): print(f"卖出门票1张") self.sellTicket1() #在类内部通过"self.函数"访问带参数self的函数 zoo=Zoo() zoo.sellTicket2() #在类外部通过"对象名.函数"访问带参数self的函数 输出: 卖出门票1张 门票一次性卖出50张 由于self代表实例对象,因此self里的变量属于实例对象,而不属于类,与类没有什么联系。举个例子,假如定义好了动物园类,则读者可以把self对象想象成某个具体的动物园,例如可以把self对象想象成厦门某个动物园,那么self对象的所有变量都跟厦门的这个动物园相关,而不会影响动物园类里的变量,对象里的变量和类里的变量是相互独立的。 1) 创建并使用self的实例变量 可以用“self.变量名”的方式在类中创建self的实例变量,self的实例变量只能在类中带参数self的函数里创建,且该函数必须被调用1次后self的实例变量才能够被正常使用,否则程序会报错。self后跟的变量名由用户自己定义,只要符合Python命名规则即可,在类内部要使用创建好的self的实例变量仍然可以通过“self.变量名”来调用。在类的外部,在类实例化产生的对象中,只需使用“对象名.变量名”就可以成功调用self的实例变量,且self的实例变量在类外部只能通过对象访问。 类实例对象访问self的实例变量的示例代码如下: //Chapter5/5.2.1_test8.py 输入: class Zoo: def sellTicket(self): #带有参数self的sellTicket(self)函数 self.tickets=500 #通过"self.变量名"定义一个self的实例变量tickets #在类中使用self的实例变量tickets,仍然要通过"self.变量名"来使用 print(f"卖出门票1张,剩下: {self.tickets-1}") zoo=Zoo() #创建一个zoo对象 zoo.sellTicket() #执行sellTicket()函数后self的实例变量tickets才算成功创建 #在类外部通过"对象名.变量名"访问实例变量tickets print("原始门票数(self实例变量tickets值)为",zoo.tickets) 输出: 卖出门票1张,剩下: 499 原始门票数(self实例变量tickets值)为 500 如果在类的外部不先执行sellTicket(self)函数,则可以直接在对象中通过“对象名.变量名”的方式调用self的实例变量,示例代码如下: //Chapter5/5.2.1_test9.py 输入: class Zoo: def sellTicket(self): self.tickets=500 #在sellTicket(self)函数中定义self的实例变量 print(f"卖出门票1张,{self.tickets-1}") zoo=Zoo() #创建一个zoo对象 #先不执行sellTicket(self)函数,而直接在类外部访问实例变量tickets print("self实例变量tickets值为",zoo.tickets) 输出: Traceback (most recent call last): File "D:/myPython/test.py", line 7, in print("self实例变量tickets值为",zoo.tickets) AttributeError: 'Zoo' object has no attribute 'tickets' 上述代码结果表明,self的实例变量tickets虽然被定义在带参数self的sellTicket(self)函数中,但该函数必须被手动执行1次后才能使self的实例变量tickets成功创建,否则会报错。 创建一个变量还需要手动执行1次函数,这样的机制实在太过麻烦,会给编程带来巨大的困扰。为了解决这一问题,Python在类中还提供了一个默认存在的__init__(self)函数。 2) __init__(self)函数概念及使用 __init__(self)函数被称为初始化函数,在Python类中被默认存在,这个函数在对象被创建时会自动地调用。如果能够让self的实例变量在这个函数里定义,则在对象创建的同时,__init__(self)函数被自动调用,self的实例变量也就同时自动地创建好了,不需要用户再手动地执行1次函数,这样就实现了一个self的实例变量初始化功能。 虽然读者在进行类定义的时候不写__init__(self)函数程序也不会报错,但Python每创建1个对象,还是会自动地默认有__init__(self)函数并执行,读者在进行类定义的时候应该养成写__init__(self)函数的好习惯。__init__(self)函数初始化self的实例变量的使用方法如下: //Chapter5/5.2.1_test10.py 输入: class Zoo: #定义一个初始化函数,将self的实例变量tickets值初值化为500 def __init__(self): self.tickets=500 #self的实例变量tickets在这里定义 zoo=Zoo() #创建一个zoo对象 #在类外部通过"对象名.变量名"访问实例变量tickets print("self实例变量tickets值为",zoo.tickets) 输出: self实例变量tickets值为 500 在上述代码中,通过Zoo类实例化zoo对象的同时,__init__(self)也被调用,进而使self的实例变量tickets被成功创建,通过zoo.tickets可以直接访问,程序能够正常运行。 3) 利用__init__(self)函数动态初始化self的实例变量 假设定义好了一个动物园类,用动物园类实例化对象,例如实例化北京某个动物园对象、实例化厦门某个动物园对象,这两个动物园对象的门票(变量tickets)初始时的数量是不同的,如果北京的这个动物园有600张门票,则应该将门票tickets初始化为600,厦门的这个动物园有500张门票,则应该将门票tickets初始化为500。由于不同对象要求的初始化值不同,这时候按照上述介绍的初始化方法就难以满足这个任务需求了,这时该怎么办呢? 解决办法其实很简单,假如我们能够在__init__(self)中添加更多的参数,通过这些参数在类外部动态地传入初始化要求的值就可以了,如我们可以将初始化函数定义成__init__(self,number),将初始值动态地通过参数number传入,再用number的值初始化self的实例变量就可以了,如可以为参数number传入初始值600,再通过self.tickets=number的方式初始化self的实例变量tickets,当然number也可以传入初始值500,这样一来就实现了为不同对象初始化不同的变量值了。 由于__init__(self,number)函数在创建对象时会被自动调用,我们从来没有显式地调用过__init__(self,number)函数,那么这个参数值number又该怎样传入__init__(self,number)函数呢? 前面提到的创建对象的语法格式为“类名()”,实际上“类名()”的“()”中还可以定义参数,则创建对象的语法格式实际为 “类名(参数1[,参数2...])”,“类名(参数1[,参数2...])”中的所有参数与__init__(self,参数1[,参数2...])函数除参数self外的所有参数一一对应,因此将number对应的参数值传入“类名(参数1[,参数2...])”的参数中就相当于将number对应的参数值传入了__init__(self,参数1[,参数2...])函数的参数中。创建对象过程中的参数值传递流程如图51所示。 图51初始化self实例变量的参数值传递过程 因此可以在zoo=zoo(number)创建zoo对象的同时,将number作为参数值传入,进而传递到__init__(self,number)函数中的参数number位置,最后通过self.tickets=number实现对self实例变量tickets的初始化赋值。通过动物园类实例化北京某动物园对象及厦门某动物园对象的示例代码如下: //Chapter5/5.2.1_test11.py 输入: class Zoo: def __init__(self,number): #定义一个初始化函数 self.tickets=number #通过参数number初值化self的实例变量tickets #创建对象的语法格式为"类名(self外的所有参数)" BeijingZoo=Zoo(500) #创建一个北京某动物园对象,500对应参数number XiamenZoo=Zoo(100) #创建一个厦门某动物园对象,100对应参数number print("北京某动物园门票有",BeijingZoo.tickets) print("厦门某动物园门票有",XiamenZoo.tickets) 输出: 北京某动物园门票有 500 厦门某动物园门票有 100 4. self的实例变量与类变量的区别 类变量是在类中直接定义的变量, 可以直接由类调用,也可以由实例对象调用。 self的实例变量是通过“self.变量名”在带self的类函数中创建的, 只能由实例对象调用, 示例代码如下: //Chapter5/5.2.1_test12.py 输入: class Zoo: tickets1=100 #类中直接定义的变量 def __init__(self): self.tickets2=500 #self的实例变量 zoo=Zoo() #实例化对象zoo 在上述代码中,tickets1是类中直接定义的变量,tickets2是self的实例变量。 假设存在Zoo类、Zoo类的实例化对象zoo、类变量tickets1初始值为0、self的实例变量tickets2初始值为0,则类变量tickets1与self的实例变量tickets2的区别和共同点如表53所示。 表53类变量tickets1与self的实例变量tickets2的区别和共同点 序号 区别和共同点 详 细 说 明 1在类体里的使用方式不同 类变量tickets1通过“类名.变量名”的方式使用,如Zoo.tickets1 self的实例变量tickets2 通过“self.变量名”的方式使用,如self.tickets2,且只能定义在类中带self参数的函数里,该函数必须被执行一次才算正式创建了self的实例变量 2在对象中的访问方式相同 类变量tickets1 self的实例变量tickets2 都是通过“对象名.变量名”的方式访问,如zoo.tickets1、zoo.tickets2,但通过“对象名.变量名”的方式是无法修改类变量tickets1的,如执行语句zoo.tickets1=zoo.tickets1+1,类变量tickets1的值通过语句Zoo.tickets1的返回结果仍然是初始值0,类变量值的改变只能通过“类名.变量名” 3在类的外部使用方式不同 类变量tickets1可以在类的外部通过“类名.变量名”的方式直接使用,如Zoo.tickets1 self的实例变量tickets2 无法在类的外部直接使用,必须先通过类实例化一个对象,再通过对象才能在外部使用,如zoo.tickets2 5. self的实例变量与类变量的使用场景 我们已经知道怎样使用self的实例变量和类变量,也知道了它们之间的区别和共同点,接下来读者可能最想知道的就是什么情况下该使用self的实例变量呢?什么情况下该使用类变量呢? 假如我们使用动物园类实例化了北京某个动物园、深圳某个动物园和厦门某个动物园,这3个动物园都能够直接访问动物园类中定义的类变量,先看如下代码: //Chapter5/5.2.1_test13.py 输入: class Zoo: count=0 #类变量count,初始值为0 def __init__(self): #初始化 pass def updateAdd(self,number): Zoo.count=number #通过"类名.变量名"将类变量count的值修改为number def getCount(self): return Zoo.count #返回类变量count的值 print(f"类变量count的初始值为{Zoo.count}") BeijingZoo=Zoo() #实例化北京某动物园对象 ShenzhenZoo=Zoo() #实例化深圳某动物园对象 XiamenZoo=Zoo() #实例化厦门某动物园对象 BeijingZoo.updateAdd(100) #BeijingZoo对象将类变量count的值修改为100 print(f"BeijingZoo对象修改类变量count的值为{count}") count=ShenzhenZoo.getCount() #ShenzhenZoo对象访问类变量count的值 print(f"ShenzhenZoo对象访问类变量count获得的值为{count}") count=XiamenZoo.getCount() #XiamenZoo对象访问类变量count的值 print(f"XiamenZoo对象访问类变量count获得的值为{count}") 输出: 类变量count的初始值为0 BeijingZoo对象修改类变量count的值为100 ShenzhenZoo对象访问类变量count获得的值为100 XiamenZoo对象访问类变量count获得的值为100 在上述代码中,有BeijingZoo、ShenzhenZoo、XiamenZoo 3个对象,类变量count的初始值为0,BeijingZoo对象通过“类名.变量名”的方式将类变量count的值修改为100,ShenzhenZoo、XiamenZoo对象再次访问类变量count获得的值是BeijingZoo对象修改后的值100,也就是说通过“类名.变量名”的方式改变类变量count的值,类变量count的值被永久性地修改,其他对象获取的类变量值随之改变,可以说多个对象共享了类变量count的值。 我们来看如果将上述代码中的count修改成self的实例变量,结果会发生什么,示例代码如下: //Chapter5/5.2.1_test14.py 输入: class Zoo: def __init__(self): #将self的实例变量count值初始化为0 self.count=0 def updateAdd(self,number): self.count=number #通过"self.变量名"将count的值修改为number def getCount(self): return self.count #返回self的实例变量count的值 print(f"self的实例变量count的初始值为0") BeijingZoo=Zoo() #实例化北京某动物园对象 ShenzhenZoo=Zoo() #实例化深圳某动物园对象 XiamenZoo=Zoo() #实例化厦门某动物园对象 BeijingZoo.updateAdd(100) #BeijingZoo对象将self的实例变量的值修改为100 print("BeijingZoo对象将self的实例变量count的值修改为{count}") count=ShenzhenZoo.getCount() #ShenzhenZoo对象访问self的实例变量count的值 print(f"ShenzhenZoo对象访问self的实例变量count获得的值为{count}") count=XiamenZoo.getCount() #XiamenZoo对象访问self的实例变量count的值 print(f"XiamenZoo对象访问self的实例变量count获得的值为{count}") 输出: self的实例变量count的初始值为0 BeijingZoo对象将self的实例变量count的值修改为100 ShenzhenZoo对象访问self的实例变量count获得的值为0 XiamenZoo对象访问self的实例变量count获得的值为0 在上述代码中,self的实例变量count初始值为0,BeijingZoo对象通过“self.变量名”的方式将self的实例变量count值修改为100,ShenzhenZoo、XiamenZoo对象再次访问self的实例变量count获得的值仍然是count的初始值0,而不是BeijingZoo对象修改后的值100,也就是说通过“self.变量名”的方式改变self的实例变量count的值,只影响当前实例对象,而不会对其他实例对象产生影响,self的实例变量不存在共享关系。 综上所述,当多个对象需要共享使用一些值时,这些值应该被定义为类变量,不需要共享则定义self的实例变量。 5.2.2类变量和类方法的权限 表53中提到类变量在类内部和类外部都可以通过“类名.变量名”的方式进行访问,但这样的访问方式很不安全,如果类变量存储的是用户的密码,而密码能够在程序里直接通过“类名.变量名”访问非常容易造成密码泄露。为提高类变量的私密性及限制类变量的权限,类变量又可以根据私密程度划分为公有变量、保护变量和私有变量,当然,类方法也可以划分为公有方法、保护方法和私有方法,前面介绍的所有类中的变量、方法都是公有变量、方法。 在Python的类中,开头用两个下画线定义一个私有变量,如__tickets=500,__tickets就是一个私有的类变量,此类变量不能在类外部直接访问(类外部不能用“类名.变量名”访问,也不能用“对象名.变量名”访问),在类内部可以通过“self.__变量名”访问。 访问私有变量的示例代码如下: //Chapter5/5.2.2_test15.py 输入: class Zoo: __tickets=100 #类中直接定义的私有变量 def __init__(self): pass def sellTicket(self): self.__tickets=self.__tickets-1 #通过self访问私有变量 print(f"卖出门票1张,还剩下{self.__tickets}张") zoo=Zoo() #实例化对象zoo zoo.sellTicket() 输出: 卖出门票1张,还剩下99张 如果在类外部访问私有变量,则程序会报错,示例代码如下: //Chapter5/5.2.2_test16.py 输入: class Zoo: __tickets=100 #类中直接定义的私有变量 def __init__(self): pass print(Zoo.__tickets) #在类外部通过"类名.变量名"直接访问私有变量 输出: Traceback (most recent call last): File "D:/myPython/test.py", line 5, in print(Zoo.__tickets) #在类外部直接访问私有变量 AttributeError: type object 'Zoo' has no attribute '__tickets' Python类中的私有方法名称同样通过两个下画线开头,如__sellTicket(self),在类内部可以通过“self.__函数”访问,不能在类外部被实例化的对象访问(类外部不能用“对象名.函数”访问)。 访问私有方法的示例代码如下: //Chapter5/5.2.2_test17.py 输入: class Zoo: def __init__(self): pass def sellTicket(self): print(f"卖出门票1张") self.__sellTicket2() #在类中通过"self.__函数"访问私有方法 def __sellTicket2(self): #定义一个私有方法 print(f"门票一次性卖出50张") zoo=Zoo() zoo.sellTicket() 输出: 卖出门票1张 门票一次性卖出50张 在类外部通过对象直接访问私有方法程序会报错,示例代码如下: //Chapter5/5.2.2_test18.py 输入: class Zoo: def __init__(self): pass def __sellTicket(self): pass zoo=Zoo() #实例化对象zoo zoo.__sellTicket() #在类外部,通过对象调用类中的私有方法 输出: Traceback (most recent call last): File "D:/myPython/test.py", line 7, in zoo.__sellTicket() #在类外部,通过对象调用类中的私有方法 AttributeError: 'Zoo' object has no attribute '__sellTicket' Python用一个下画线作为开头定义类中的保护变量和保护方法,如_tickets代表保护变量、 _sellTicket(self)代表保护方法,保护变量和保护方法可以像公有变量和公有方法一样在类的内部和外部正常访问,但作为模块内容导入另一个Python文件中时不能被访问调用。 5.2.3综合示例 【例51】请编写一个矩形类,包含求出矩形周长和面积的方法,并求出长为10,宽为5的矩形的周长和面积。 分析: 矩形共有的特征是具有长和宽,可以将长和宽抽象出来作为矩形类的属性,因为不同矩形的长、宽并不一样,可以采用self的实例变量来定义矩形的长和宽。示例代码如下: //Chapter5/5.2.3_test19.py 输入: class Rectangle: def __init__(self,length,width): #初始化矩形的长和宽 self.length=length self.width=width def perimeter(self): #定义周长方法 return 2*self.length+2*self.width def area(self): #定义面积方法 return self.length*self.width rectangle=Rectangle(10,5) #实例化rectangle对象,传入矩形长10、宽5 perimeter=rectangle.perimeter() #调用求出矩形周长的方法 area=rectangle.area() #调用求出矩形面积的方法 print(f"长为10、宽为5的矩形的周长为{perimeter}") print(f"长为10、宽为5的矩形的面积为{area}") 输出: 长为10、宽为5的矩形的周长为30 长为10、宽为5的矩形的面积为50 【例52】请用面向对象的编程方式回答下面的问题: 假设现在有位于北京名为“动物园1号”、位于深圳名为“动物园2号”、位于厦门名为“动物园3号”的3个动物园。 动物园1号里有大熊猫、乌龟、犀牛和山羊共4种动物,门票35元一张,每个月开放25天,开放第1天的人流量为100人,此后每天的人流量都比前一天多20%; 动物园2号里有大雁、猴子和羚羊共3种动物,门票38元一张,每个月开放23天,开放第1天的人流量为120人,此后每天的人流量都比前一天多23%; 动物园3号里有白鹤、天鹅和白狐共3种动物,门票32元一张,每个月开放28天,开放第1天的人流量为80人,此后每天的人流量都比前一天多20%。 3个动物园要把每天门票收入的20%捐赠给公益组织以便保护更多的野生动物,65%要用来维持动物园运营的成本,剩下的15%将作为员工的收入,请问3个动物园每个月的捐赠、运营成本、员工收入分别是多少钱?注: 最终的所有结果采用四舍五入法保留两位小数。 分析: 3个动物园的名称、拥有的动物种类、门票价格、人流量的初始值、每天人流量变化的百分比和开放日天数是3个动物园的共同特征,可以抽象为动物园类的属性,计算人流量、卖票获得收入、公益捐赠、计算运营成本、计算员工收入则可以抽象为动物园类的方法。示例代码如下: //Chapter5/5.2.3_test20.py 输入: class Zoo: def __init__(self,name,species,tickets,count,percent,days): self.name=name #定义动物园名称 self.species=species #定义动物园动物的种类,为列表类型 self.tickets=tickets #定义动物园的票价 self.count=count #定义动物园人流量的初始值 self.percent=percent #定义动物园每天人流量变化的百分比 self.days=days #定义动物园开放日的天数 def countPeople(self): #定义计算人流量方法 sum=self.count #定义一个累加器,初始值为self.count temp=self.count #用temp来记录人流量的变化 for i in range(self.days-1): temp=temp*(1+self.percent) sum=sum+temp return sum def getTickets(self): #定义计算卖票收入的方法 sum=self.countPeople() #获取人流总量 return self.tickets*sum #返回票价*人流量总量 def donate(self): #定义公益捐赠的方法 return self.getTickets()*0.2 #返回票价收入的20% def cost(self): #定义计算运营成本的方法 return self.getTickets()*0.65 #返回票价收入的65% def salary(self): #定义计算员工收入的方法 return self.getTickets()*0.15 #返回票价收入的15% BeijingSpecies=["大熊猫","乌龟","犀牛","山羊"] BeijingZoo=Zoo("动物园1号",BeijingSpecies,35,100,0.2,25) #动物园1号 print(f"下面是动物园1号: \n动物种类有{BeijingSpecies}") donate=round(BeijingZoo.donate(),2)#四舍五入保留两位小数 cost=round(BeijingZoo.cost(),2) salary=round(BeijingZoo.salary(),2) print(f"每个月捐赠:{donate},运营成本:{cost},员工收入:{salary}") ShenzhenSpecies=["大雁","猴子","羚羊"] ShenzhenZoo=Zoo("动物园2号",ShenzhenSpecies,38,120,0.23,23) #动物园2号 print(f"下面是动物园2号: \n动物种类有{ShenzhenSpecies}") donate=round(ShenzhenZoo.donate(),2) cost=round(ShenzhenZoo.cost(),2) salary=round(ShenzhenZoo.salary(),2) print(f"每个月捐赠:{donate},运营成本:{cost},员工收入:{salary}") XiamenSpecies=["白鹤","天鹅","白狐"] XiamenZoo=Zoo("动物园3号",XiamenSpecies,32,80,0.2,28) #动物园3号 print(f"下面是动物园3号: \n动物种类有{XiamenSpecies}") donate=round(XiamenZoo.donate(),2) cost=round(XiamenZoo.cost(),2) salary=round(XiamenZoo.salary(),2) print(f"每个月捐赠:{donate},运营成本:{cost},员工收入:{salary}") 输出: 下面是动物园1号: 动物种类有['大熊猫', '乌龟', '犀牛', '山羊'] 每个月捐赠:330386.76,运营成本:1073756.96,员工收入:247790.07 下面是动物园2号: 动物种类有['大雁', '猴子', '羚羊'] 每个月捐赠:459571.95,运营成本:1493608.85,员工收入:344678.96 下面是动物园3号: 动物种类有['白鹤', '天鹅', '白狐'] 每个月捐赠:419442.34,运营成本:1363187.59,员工收入:314581.75 5.3类的继承 5.3.1继承的概念 面向对象编程因为有类的存在而灵活多变,当一些类的属性和方法存在交集时,为了避免重复定义这些属性和方法,可以通过类的继承机制来提高代码的复用性,从而减轻编程人员的负担。 类的继承描述的是两个类之间的关系,正如现实生活中的很多概念都具有包含与被包含的关系,例如猫包含于动物类。猫本身也可以作为一个类别存在,即猫类,由于猫类与动物类之间的包含与被包含关系,我们可以称猫类为动物类的子类,动物类为猫类的父类。 猫类(子类)具有动物类(父类)的绝大多数特点和功能。假设我们在Python代码中定义了动物类,如果还要定义猫类,猫类不仅要定义动物类中的绝大多数属性和方法,还要再定义一些独有的属性和方法,这样定义就显得代码非常冗余,做了大量的重复工作。为此,我们在定义猫类时可以通过继承的方法,让猫类继承动物类已经定义好的属性和方法,这样定义猫时就只需再定义猫类独有的属性和方法就可以了。 当两个类之间具有大量的重复属性和方法或具有一定的包含与被包含关系时,我们只需要在范围更广的那个类中定义重复的属性和方法(称这个类为父类、基类或超类),让另一个范围更小的类继承这些属性和方法即可(称这个类为子类或派生类),通过继承机制子类就拥有了父类的绝大多数属性和方法,从而避免了重复定义。 5.3.2继承的语法和使用 1. 类的继承语法: 类的继承语法格式如下: class子类名(父类名1,父类名2...) 上述语法格式代表着该类将作为子类同时继承括号内声明的所有父类,该类将具有所有父类的绝大多数属性和方法,继承的使用示例代码如下: //Chapter5/5.3.2_test21.py 输入: class Animal: #定义父类: 动物类 age=3 #定义父类属性: age def __init__(self): print("调用父类初始化函数:动物类") def eating(self): print('调用父类Animal的吃饭方法: 动物都会吃饭') def setage(self, age): print('将属性age设置为:',age) Animal.age = age def getage(self): print("获取属性age:", Animal.age) class Cat(Animal): #定义子类: 猫类,猫类继承动物类 def __init__(self): print("调用子类初始化函数: 猫类") def catching_mouse(self): print('调用子类的独有方法: 抓老鼠') cat=Cat() #实例化子类,获得猫类对象cat cat.catching_mouse() #调用子类对象cat的方法 cat.eating() #通过子类对象cat调用父类的吃饭方法 cat.setage(10) #通过子类对象cat调用父类的设置属性值方法 cat.getage() #通过子类对象cat调用父类的获取属性值方法 输出: 调用子类初始化函数: 猫类 调用子类的独有方法: 抓老鼠 调用父类Animal的吃饭方法: 动物都会吃饭 将属性age设置为: 10 获取属性age: 10 在上述代码中,子类猫Cat继承了父类动物Animal的属性age,以及3种方法(eating()、setage()和getage())。继承之后子类对象就可以直接调用父类的属性及方法了。子类猫Cat实例化产生对象cat,对象cat先调用了本类猫的独有方法: catching_mouse(),又调用了父类动物Animal的3种方法: eating()、setage()和getage()。值得注意的是,通过子类对象调用父类方法时,Python总是先从子类中开始查找该方法,如果不能在子类中找到对应的方法,则到父类中逐个查找。子类和父类中都有初始化函数,当子类实例化一个对象时,Python先在子类中寻找初始化函数,由于可以在子类中找到初始化函数,因此子类实例化一个对象时输出子类的构造函数,而不会调用父类的初始化函数,这种父类和子类具有相同名称、函数体却内容不同的情况,被称为方法重写,方法重写将在后面的内容中介绍。 当然,上述代码只展示了子类继承一个父类的情形,子类继承多个父类的示例代码如下: //Chapter5/5.3.2_test22.py 输入: class Animal: #定义父类: 动物类 age=3 #定义父类属性: age def __init__(self): print("调用父类(动物类)初始化函数") def eating(self): print('调用父类(动物类)的吃饭方法: 动物都会吃饭') class CuteThings: #定义一个父类: CuteThings def __init__(self): print("调用父类初始化函数:可爱的东西") def they_are_cute(self): print('调用父类(CuteThings)的方法: 这种东西都很可爱') class Cat(Animal,CuteThings): #定义子类猫: Cat,继承父类Animal和CuteThings def __init__(self): print ("调用子类(猫类)初始化方法") def catching_mouse(self): print ("调用子类(猫类)方法: 抓老鼠") cat= Cat() #实例化子类 cat.catching_mouse() #调用子类本身的方法 cat.eating() #调用父类Animal方法 cat.they_are_cute() #调用父类cutethings方法 输出: 调用子类(猫类)初始化方法 调用子类(猫类)方法: 抓老鼠 调用父类(动物类)的吃饭方法: 动物都会吃饭 调用父类(CuteThings)的方法: 这种东西都很可爱 在上面例子中子类Cat继承了两个父类(类CuteThings和类Animal),子类Cat可以调用这两个父类的所有属性和方法。 2. 子类对父类方法的重写 很多时候父类中定义的方法无法满足子类的需求,如虽然动物都具有吃饭的功能方法,但是有些动物食草,而有些动物食肉,因此食草、食肉动物子类的吃饭方法应与父类动物类中的吃饭方法不同,在定义这样的子类时要对父类方法重新编写。子类中对父类中的方法进行重新编写的过程就叫作方法的重写,示例代码如下: //Chapter5/5.3.2_test23.py 输入: class Animal: #定义父类: 动物类 age=3 def __init__(self): print("调用父类(动物类)构造函数") def eating(self): print("调用父类(动物类)方法: 动物都会吃饭") class Herbivore(Animal): #定义子类: 食草动物类,继承父类动物类 def __init__(self): print ("调用子类(Herbivore)初始化方法") def eating(self): #重写父类中的eating()方法,方法名称与父类方法一致 print ("重写父类eating()方法: 食草动物吃草") herbivore=Herbivore() #实例化子类 herbivore.eating() #调用子类中对父类重写的方法 输出: 调用子类(Herbivore)初始化方法 重写父类eating()方法: 食草动物吃草 在上述例子中,父类动物类Animal中的方法eating()输出“调用父类方法: 动物都会吃饭”,显然这并不满足子类食草动物类Herbivore特殊化的食草要求,因此我们可以利用重写机制,即在子类中重新定义与父类方法同名的方法。在食草动物类Herbivore重写eating()方法后,实例化一个子类对象herbivore,子类对象herbivore在调用方法eating()时,会先在子类中查找该方法,进行方法重写后子类中有了新的eating()方法,因此无须继续在父类中寻找eating方法,直接调用子类中新的eating()方法,屏蔽掉了父类同名方法。 3. 子类继承父类的__init__()初始化函数 子类继承父类的初始化__init__()函数的情况比较特殊,分为3种情况: (1) 子类不重写父类的__init__()初始化函数,实例化子类时,会自动调用父类定义的__init__()初始化函数,示例代码如下: //Chapter5/5.3.2_test24.py 输入: class Animal: #定义父类: 动物类 def __init__(self,name): #父类初始化函数 self.name=name #初始化动物名称 print("调用父类初始化函数,为动物取名为",self.name) class Cat(Animal): #定义子类猫,继承动物类,子类中并没有重写构造函数 def getName(self): return self.name #返回父类中初始化的动物名称 cat= Cat("mimi") #实例化子类 print("猫咪的名字是: ",cat.getName()) 输出: 调用父类初始化函数,为动物取名为 mimi 猫咪的名字是: mimi 在上述代码中,子类Cat并没有重写父类Animal中的构造函数,因此子类Cat在实例化对象cat时,调用的初始化函数是父类的初始化函数。 (2) 如果子类重写了父类的__init__()初始化函数,在实例化子类时,就不会调用父类已经定义的 __init__()初始化函数,示例代码如下: //Chapter5/5.3.2_test25.py 输入: class Animal: #定义父类: 动物类 def __init__(self,name): #父类初始化函数 self.name=name print("调用父类初始化函数,为动物取名为",self.name) class Cat(Animal): #定义子类猫,子类重写构造函数 def __init__(self,name): #子类初始化函数 self.name=name print("调用子类初始化函数,为动物取名为",self.name) def getName(self): return self.name cat=Cat("mimi") #实例化子类时调用子类中的构造函数 print("猫咪的名字是: ",cat.getName()) 输出: 调用子类初始化函数,为动物取名为 mimi 猫咪的名字是: mimi (3) 子类重写父类__init__()初始化函数时,若既要继承父类中的初始化函数,又要添加一些独特的代码内容,则可以使用super()函数,super()函数的功能是调用父类方法。假设父类Animal中有一个eating()函数,在子类中可以通过super().eating()的方式调用父类Animal中的eating()方法,因此我们也可以在子类的__init__()初始化函数中通过super()函数先调用父类中的初始化函数,再编写子类初始化时独有的代码内容,通过super()函数调用父类初始化方法的语法格式如下: super(子类名称,self).__init__(参数1,参数2,....) 示例代码如下: //Chapter5/5.3.2_test26.py 输入: class Animal: #定义父类: 动物类 def __init__(self,name): #父类初始化函数 self.name=name print("调用父类初始化函数,为动物取名为",self.name) class Cat(Animal): #定义子类: 猫类 def __init__(self,name): #子类构造函数 super(Cat,self).__init__(name) #通过super()函数调用父类的初始化函数 self.name=name print("调用子类初始化函数,为动物取名为",self.name) def getName(self): return self.name cat=Cat("mimi") #实例化子类时调用子类中的构造函数 print("猫咪的名字是: ",cat.getName()) #调用子类的getName()函数 输出: 调用父类初始化函数,为动物取名为 mimi 调用子类初始化函数,为动物取名为 mimi 猫咪的名字是: mimi 因为在子类的初始化函数中使用super()函数强制调用了父类的初始化函数,因此子类在实例化子类对象的过程中,在调用子类初始化函数的同时,也会调用父类的初始化函数,然后继续执行子类初始化函数的剩余部分。 5.4Python中的异常处理机制 5.4.1异常的概念 在编写代码的过程中可能会遇到各种各样的错误,一旦程序报错就会给我们带来巨大的困扰,初学者很难搞清楚代码为什么不能成功运行。 代码不能成功运行可能来源于以下两个方面: 一是初学者较为频繁碰到的解析错误,或称为语法错误。语法错误往往由遗漏分号、冒号或者程序中掺杂了中文符号导致。在发生该类错误时,解析器往往能够明显定位程序发生语法错误的位置,使该类问题较容易解决。 二是程序逻辑错误,即使Python程序语法正确,但由于运行过程中程序过于复杂或逻辑不清,导致一些意料之外的错误,这类错误也称为异常,这是本节将要详细介绍的内容。由于程序出现异常的原因往往与编程人员的思维逻辑有很大相关性,因此异常纠正较为费时费力,不像解析错误一样容易解决,但大多数编程语言都为解决异常提供了处理机制,即异常处理机制。 异常处理机制能够将可能出现异常的代码段和正常功能代码段进行很好分割,从而使代码整体结构更加简洁明了,可读性更强,并且利用异常处理机制能够精准定位异常代码出现的位置及产生异常的原因,对于非致命性异常能够将其延迟抛给对应的程序层面处理。 5.4.2异常处理语句 在4.1.2节函数声明的语法格式中,读者接触过了函数中的return语句,return语句可以将程序结果返回到程序外部,从另一个角度思考,return语句是不是也能将程序执行后可能产生的异常值返回呢?当然可以,return语句返回异常值就是一种异常处理机制。若函数运行时程序发生了异常,人们往往会通过return语句返回一个约定的常数值(如常数-1)用来表示函数运行过程中产生的某种错误,示例代码如下: //Chapter5/5.4.2_test27.py 输入: def func(x): #定义func()函数 if x>0: #假设x>0时程序正常执行 print("程序正常执行") else: #否则 return -1 #返回-1,表示程序没有正常执行 a=func(-20) if a==-1: #如果func(-20)则返回-1 print("func()函数运行出错") 输出: 函数func运行出错 上述代码在调用func()函数的过程中出现错误,通过return语句返回了约定的异常值-1(-1代表产生了某种错误),否则返回函数正常的执行结果,但随着程序代码复杂性的增强,编程人员若每次编写函数都要通过这样的方式返回约定的异常值,会给程序编程带来巨大的工作量,使用也不方便,因此有了异常处理语句。 1. try/except语句 为了更好地对异常进行处理,Python提供try/except语句专门解决程序异常问题,try/except语句的语法格式有3种,最简单的一种语法格式如下: try: 代码块1 except: 代码块2(代码块1检测出异常后才执行的代码块) 在上述语法格式中,首先会执行try与except语句之间的代码块1,如果代码块1的执行结果无异常发生则会忽略except语句里的代码块2,若发生异常则代码块1中发生异常的该行代码之后所有的剩余语句将被忽略,except语句里的代码块2将被执行,示例代码如下: //Chapter5/5.4.2_test28.py 输入: try: file=open("Python.txt") #打开当前目录下的一个文件,假设该文件不存在 except: print("发生了异常,该文件不在当前目录下") 输出: 发生了异常,该文件不在当前目录下 在上述代码中,首先会正常执行open()函数,若open()函数没能通过传入的文件目录路径找到文件,则程序会报错(发生异常)导致程序终止,通过try/except语句将open()函数包围后,当open()函数发生异常时不会导致程序终止,而是会转而执行except里的代码块内容。若上述代码不用try/except语句包围,则示例结果如下: //Chapter5/5.4.2_test29.py 输入: file=open("Python.txt") #打开当前目录下名为"Python.txt"的文件,假设该文件不存在 输出: Traceback (most recent call last): File "D:/myPython/test.py", line 1, in file=open("Python数据分析从0到1.txt") FileNotFoundError: [Errno 2] No such file or directory: 'Python.txt' 上述代码执行结果报错,执行结果报错会导致整个程序停止运行,编程人员显然并不希望因为一个小异常导致整个程序停止运行,因此try/except语句对异常的捕捉显得非常关键。 通过上述try/except语句的语法格式只能帮助我们捕捉到程序异常,而无法告诉我们为什么会发生这个异常,异常的种类是什么,为此我们可以在except语句后添加异常的种类,只需将异常名称放在一个括号中成为一个元组(如果只有一个异常名称则无须加括号,直接写即可),语法格式如下: try: 代码块1 except (异常名1,异常名2,异常名3,...): #如果只有1个异常名,则语法格式为except 异常名 代码块2(代码块1检测出异常后才执行的代码块) 如果程序发生的异常类型与except语句后的异常类型成功匹配,则可以直接打印出异常类型,示例代码如下: //Chapter5/5.4.2_test30.py 输入: try: file=open("Python.txt") #打开当前目录下的一个文件,假设该文件不存在 except FileNotFoundError: print(format(FileNotFoundError)) 输出: 在上述代码中,open()函数发生异常对应匹配的异常类型为FileNotFoundError,与except语句后跟的异常类型名称成功匹配,因此异常FileNotFoundError成功被捕捉,但如果异常类型没有成功匹配,则程序仍然会报错但会停止运行,示例代码如下: //Chapter5/5.4.2_test31.py 输入: try: file=open("Python.txt") #假设"Python.txt"文件实际不存在 except InterruptedError: print(format(InterruptedError)) 输出: Traceback (most recent call last): File "D:/myPython/test.py", line 2, in file=open("Python.txt") FileNotFoundError: [Errno 2] No such file or directory: 'Python.txt' 另外一个try语句后可以连接多个except语句,可以分别处理不同的异常,这些except语句一次只能执行一个,不会同时被执行,同时最后一个except语句后一般不带有异常名称,可以用来解决上述代码未找到对应匹配异常导致程序停止运行的问题,也往往通过最后一个except语句来打印出难以预计的异常类型信息,且最后一个except语句里的代码块内容结尾还可以跟一个raise语句,raise语句表示将异常再次抛出,让编程人员能够知道发生了哪个种类的异常,但会导致程序停止运行,语法格式如下: try: 代码块1 except异常1: 代码块2#发生了异常1才会执行 except异常2: 代码块3#发生了异常2才会执行 ... except: 代码块n raise#raise语句不是必须写的 示例代码如下: //Chapter5/5.4.2_test32.py 输入: try: file=open("Python.txt") #假设"Python.txt"文件不存在 except InterruptedError: print(format(InterruptedError)) except: #即使上述异常不匹配也不会导致程序报错 print("发生了难以预计的异常") 输出: 发生了难以预计的异常 2. try/except...else语句 在try/except语句末尾还可以添加else语句,如果程序无异常发生,则会正常执行else语句里的代码,否则不执行,语法格式如下: try: 代码块1 except 异常1: 代码块2 ... else: 代码块n #程序无异常才会执行 示例代码如下: //Chapter5/5.4.2_test33.py 输入: try: print("try语句里的代码无异常") except InterruptedError: print(format(InterruptedError)) else: print("else语句里的代码正常执行") 输出: try语句里的代码无异常 else语句里的代码正常执行 3. try/except...else/finally语句 在编程过程中总希望不论程序是否存在异常,有一部分代码仍然能正常执行,由此看来try/except...else语句也具有一定的局限性,为此我们可以将finally语句放在整个try/except语句最后,表示无论try语句里的代码是否产生异常,finally语句里的代码都会被执行,语法格式如下: try: 代码块1 except 异常1: 代码块2 ... else: 代码块n-1 #程序无异常才会执行 finally: 代码块n #无论程序是否存在异常都会执行 示例代码如下: //Chapter5/5.4.2_test34.py 输入: try: print("try语句里的代码无异常") except InterruptedError: print(format(InterruptedError)) else: print("执行else语句里的代码") finally: print("finally语句里的代码,无论是否有异常都会执行") 输出: try语句里的代码无异常 执行else语句里的代码 finally语句里的代码,无论是否有异常都会执行 Python中的异常处理语句总结如表54所示。 表54Python异常处理语句总结 序号 语句名称 详 细 说 明 1 try try语句里的代码正常执行,如果某一行代码产生了异常,则语句里的代码会停止执行 2 except try语句里的代码产生了异常才执行,如果没有异常则不执行 3 else try语句里的代码没有异常才执行,如果产生了异常则不执行 4 finally 无论try语句里的代码是否执行,finally里的代码都会被执行 5 raise raise语句表示主动将异常抛出 4. Python中的异常类型 Python中的异常类型是以类的形式存在的,上述代码中出现过的异常类型InterruptedError和FileNotFoundError都是以类的形式存在的,这些异常类型被称为异常类。所有的异常类都继承自父类BaseException。Python中具有很多异常父类,异常父类在表55中以父类序号的形式表示,例如表55序号6表示的类StopIteration,其父类序号为5,意为类StopIteration继承于表55序号5表示的类Exception,即类Exception是类StopIteration的父类。 Python异常类如表55所示。 表55Python异常类 序号 类名 详 细 说 明 父类序号 1 BaseException 所有异常的基本类 无 2 SystemExit 解释器请求退出程序 1 3 KeyboardInterrupt 用户通过键盘输入中断程序执行 1 4 GeneratorExit 生成器发生异常导致退出 1 5 Exception 常见常规错误的基本类 1 6 StopIteration 迭代器中无值导致停止 5 7 StopAsyncIteration 由一个特定对象的方法来停止迭代 5 8 ArithmeticError 常见数值计算错误异常基类 5 9 FloatingPointError 浮点计算错误 8 10 ZeroDivisionError 零作为除数 8 11 OverflowError 运算过程数值超过最大限制 8 12 AssertionError 断言语句出错 5 13 BufferError 缓冲区相关操作出错 5 14 EOFError 到达EOF标记 5 15 AttributeError 属性错误,可能对象无所使用属性 5 16 ImportError 导入模块或库错误 5 17 ModuleNotFoundError 无法找到对应模块 16 18 LookupError 映射使用的键或索引无效 5 续表 序号 类名 详 细 说 明 父类序号 19 IndexError 序列中无该索引 18 20 KeyError 映射中无该键 18 21 MemoryError 内存错误 5 22 NameError 使用了未声明或初始化的对象 5 23 UnboundLocalError 使用了未声明的本地变量 22 24 OSError 操作系统错误 5 25 ChildProcessError 子进程操作失败 24 26 BlockingIOError 阻塞对象操作失败 24 27 ConnectionError 与连接相关的异常基类 24 28 ConnectionResetError 连接被重置 27 29 ConnectionRefusedError 连接被拒绝 27 30 ConnectionAbortedError 连接被中断 27 31 BrokenPipeError 套接字写入错误 27 32 FileNotFoundError 对不存在的文件或目录做出操作 24 33 FileExistsError 重复创建已经存在的文件或目录 24 34 InterruptedError 系统调用过程被中断 24 35 TimeoutError 程序超时 24 36 PermissionError 不具备进行操作的权限 24 37 ProcessLookupError 对不存在的进程进行操作 24 38 NotADirectoryError 对非目录进行目录操作 24 39 IsADirectoryError 对目录进行了文件操作 24 40 RunTimeError 不属于其余类别的错误 5 41 RecursionError 解释器检测超过最大递归深度 40 42 SyntaxError 语法错误的基类 5 43 IndentationError 缩进存在错误 42 44 TabError Tab和空格混用导致异常 43 45 SystemError 解析器内部异常 5 46 TypeError 操作对应的对象类型不正确 5 47 ValueError 操作对象类型的值不正确 5 48 UnicodeError Unicode编码或解码错误 47 49 UnicodeDecodeError Unicode解码错误 48 50 UnicodeEncodeError Unicode编码错误 48 51 UnicodeTranslateError Unicode转码错误 48 52 Warning 所有警告的基类 5 53 BytesWarning 有关Bytes的警告的基类 52 54 UnicodeWarning 与Unicode相关的警告的基类 52 55 ImportWarning 模块导入可能出错的警告的基类 52 56 ResourceWarning 与资源相关的警告的基类 52 57 FutureWarning 已弃用功能的警告的基类 52 58 UserWarning 用户代码生成警告的基类 52 59 SyntaxWarning 可疑语法警告的基类 52 60 DeprecationWarning 已经弃用功能的警告基类 52 5.4.3assert断言 在程序调试过程中,最为常用的方法是通过print()函数输出程序结果来判断程序是否正常运行,假如程序出现异常,通过这种方式无法让编程人员清楚异常产生的原因,只能依靠猜测,为此我们可以通过assert断言语句来代替。assert断言语句后跟一个表达式,如果表达式结果为True则表示程序正常运行,如果结果为False则会抛出异常,语法格式如下: assert表达式 示例代码如下: //Chapter5/5.4.3_test35.py 输入: assert 1==0 #结果为False时会抛出异常,并指出异常类型 输出: Traceback (most recent call last): File "D:/myPython/test.py", line 1, in assert 1==0 AssertionError 上述代码assert断言后跟的表达式的返回值为False,执行结果抛出AssertionError异常。 用户还可以为assert断言语句后添加参数,参数会作为异常提示信息一起与异常类信息输出,语法格式如下: assert表达式,参数 示例代码如下: //Chapter5/5.4.3_test36.py 输入: assert 1==0,"1==0抛出了异常" #"1==0抛出了异常"为异常提示信息 输出: Traceback (most recent call last): File "D:/myPython/test.py", line 1, in assert 1==0,"1==0抛出了异常" AssertionError: 1==0抛出了异常 5.4.4自定义异常 在编程过程中,由于不同问题的需求不同,仅依靠Python提供的异常类无法满足需求,因此往往需要自行定义才能使程序功能更为完善。在上述介绍中,我们知道异常其实就是类,因此我们也可以通过创建类的方式来自定义异常。自定义异常必须继承Python提供的异常父类(表55所介绍的父类),进而成为新的异常子类,示例代码如下: //Chapter5/5.4.4_test37.py 输入: class myError(BaseException): #继承类BaseException def __init__(self,info): self.info = info try: raise myError("自定义异常") #raise语句表示主动抛出异常 #参数"自定义异常"与类myError的__init__()函数中的参数info匹配 except myError as e: #as表示取一个别名,即为myError取别名e print("发生的异常类型为",e.info) 输出: 发生的异常类型为: 自定义异常 通过自定义异常,读者能够在编程过程中灵活使用异常机制来处理程序存在的问题,效率能够显著提升。 5.5本章小结 本章相对初学者来讲比较难以适应及接受,因此本章更多地偏向用类比的方法为读者介绍抽象的理论概论,并提供大量的示例代码供读者思考和练习。本章首先从面向对象的相关理论出发,介绍了类和对象概念,并对类和对象引出的一些其他相关概念进行了整理,接着对面向过程编程与面向对象编程的异同点进行了比较,然后介绍了类和对象语法格式及编程方式,并通过两道面向对象的综合示例编程题向读者展示利用面向对象进行编程解题时的思维方式及编程技巧,最后介绍了类的继承及Python中的异常处理机制。