第十一章 模块和包
11.1模块介绍
11.2模块的使用
11.2.1导入模块
11.2.2模块别名
11.2.隐藏模块数据
11.2.4__name__
11.3模块搜索路径
11.4模块的缓存
11.5包
11.5.1包的概念
11.5.2导入包
11.5.3__init__.py
11.5.4__all__
11.6数学模块
11.6.1math
11.6.2random
11.7日期与时间
11.7.1time
11.7.2datetime
11.8系统相关
11.8.1sys
第十二章 异常
12.1异常说明
12.1.1异常概念
12.1.2常见异常类型
12.2捕获异常
12.2.1try…except
12.2.2捕获多个异常
12.2.3else
12.2.4finally
12.手动抛出异常
自定义异常-toc" style="margin-left:40px;">12.4自定义异常
第十三章迭代器、生成器、装饰器
13.1迭代
13.1.1可迭代对象和迭代器
13.1.自定义迭代类型
13.1.3迭代合体
13.2生成器
13.2.1需求背景
13.2.生成器表达式
13.2.3生成器函数
13.3装饰器
13.3.1闭包
13.3.2需求背景
13.3.3使用装饰器
13.3.4.装饰装饰装置中含有参数
13.3.5保留函数信息
13.3.6类装饰器
第十四章 文件
14.1读写文件
14.1.1获取文件对象
14.1.2文件的读取
14.1.3文件的写入
14.1.4文件定位
14.2文件与路径的操作
14.2.1os模块
14.2.2os.path模块
14.2.3shutil模块
14.3序列化
14.3.1csv
14.3.2json
14.3.3pickle
14.4上下文管理器
14.4.1自定义上下文管理器
14.4.2@contextmanager装饰器
第十五章 正则表达式
15.1从re模块开始
15.2特殊字符
15.2.1字符相关
15.2.2次数相关
15.2.3边界相关
15.2.4组相关
15.2.5控制标记
15.3相关属性与方法
15.3.1re模块的函数
15.3.2正则表达式对象
第11章 模块与包
11.1. 模块介绍
11.2. 模块的使用
11.2.1. 导入模块
11.2.2. 模块别名
11.2.3. 隐藏模块数据
11.2.4. __name__
11.3. 模块搜索路径
11.4. 模块的缓存
11.5. 包
11.5.1. 包的概念
11.5.2. 导入包
11.5.3. __init__.py
11.5.4. __all__
11.6. 数学模块
11.6.1. math
11.6.2. random
11.7. 日期与时间
11.7.1. time
11.7.2. datetime
11.8. 系统相关
11.8.1. sys
11.1模块介绍
模块是一个Python文件,该文件包含相关的定义与语句(类,函数,变量)。模块具有名称,名称与文件的名称一致。
模块具有如下的好处:
- 通过划分若干个模块,我们就可以将项目程序进行明确的划分,从而将复杂问题简单化,同时,也能够进行更加合理的分工,有利于协作式开发程序。
- 模块提供独立的命名空间,可以解决命名上的冲突。不同的模块中,定义相同名称的变量,不会产生命名冲突。
- 模块可以实现良好的重用性,在多人间实现共享。
11.2模块 的 使用
大的项目,可以划分为多个模块。但模块与模块之间,不可能都是完全孤立的。一个模块很可能需要与其他的一个(或多个)模块进行交互,因此,如果我们需要在一个模块中使用其他模块定义的名称(函数,类,变量等),则需要首先导入该模块,然后通过模块名.名称进行访问。
11.2.1导入 模块
我们可以使用import对模块进行导入:
import 模块
我们还可以一次导入多个模块:
import 模块1,模块2……
当导入模块时,该模块内的语句就会得到执行,但只会执行一次,即重复导入模块不会多次执行。当模块作为脚本执行时(使用python命令在命令行执行),模块中的语句也会得到执行。按照惯例,模块导入语句写在模块的最上方。
当导入模块后,我们就可以使用模块中所定义的名称,格式为:
模块名.名称
我们也可以将其他模块中定义的名称(全局命名空间中的名称)直接导入到当前模块的命名空间,这样,我们就可以直接通过名称访问,而无需使用模块名限定。格式如下:
from 模块 import 名称1,名称2……
不过,此种导入方式要稍加留意,因为如果当前模块中也存在同样名称的定义,就会造成名称的冲突,也就是名称会重新绑定后来的对象。
假设我们现在不考虑名称的冲突,只考虑访问的便捷性,使用from import的方式确实是不错的选择。但是,如果我们要使用该模块中定义的很多名称,一个个的导入可能会有些繁琐,此时,我们可以使用批量导入:
from 模块 import *
这样就会将模块中除了以_开头的所有名称导入到当前模块的命名空间。然而,这种方式会导入很多名称,容易造成命名冲突,尽可能少用。
- dir([object])
11.2.2模块 别名
当导入模块(或模块中的名称)时,我们可以使用as为模块(或模块中的名称)指定别名。语法如下:
import 模块名 as 模块别名
from 模块名 import 名称 as 名称别名
这样,我们就可以通过别名来访问模块(或模块中的名称)。但是,一旦指定别名后,原模块名(或原模块中的名称)将不再可用。
指定别名具有如下好处:
- 在使用import 或 from时,如果产生命名冲突,可以使用别名来解决。
- 如果模块或名称较长,可以使用简短的别名,减少输入量。
11.2.3隐藏 模块 数据
因为使用import *的语法会在当前命名空间增加很多名称,为了减少import *所造成的影响(名称冲突),我们可以有选择性的隐藏模块的数据,进而限制使用import *时,名称的导入。
隐藏模块数据可以采用两种方式:
- 将名称以下划线(_)开头。
- 定义__all__变量。
说明
- 以上两种隐藏方式仅是限制使用import *语法导入的名称,并不是表示该名称无法在模块外访问。使用import其他方式导入,还是能够在模块外进行访问的。
如果模块中定义的名称以_开头,则在使用import *语法时,这些名称不会导入到当前模块的命名空间中,即当前模块无法直接访问该名称。
如果使用from 模块 import *的语法,则默认情况下会导入模块中除_开头的所有名称,这容易与当前模块的命名造成冲突。我们可以定义__all__变量来限制导出名称的数量。当在模块中定义__all__变量时,该变量的值为一个字符串列表,只有列表中指定的名称才会导入到当前命名空间。
当在__all__变量中指定了以下划线(_)开头的名称时,会出现什么情况呢?此时,会首先检查__all__变量,如果存在,会将__all__中指定的名称导入到当前的命名空间。如果不存在,则会将除下划线(_)开头的名称导入到当前的命名空间。由判定顺序角度可以得知:__all__中即使指定以下划线(_)开头的名称,该名称依然可以成功导入到当前的命名空间中。
11.2.4__name__
在我们编写模块时,我们可能需要对当前模块的功能进行测试。
可是,当我们在其他模块中导入当前模块时,测试内容就会得到执行,这并不是我们想要的。因此,我们不得不将其去掉或者注释。可是,在以后修改该模块时,例如,又增加了一个新的函数,我们还需要再写入新的测试代码来验证新增功能的正确性,如此反复,势必会带来一定的不方便性。
我们可以通过__name__属性来获取模块的名称。之前我们提过,模块会在两种情况下执行:
- 当模块由其他模块导入时。
- 当模块作为脚本,在命令行使用python 文件名.py执行时。
但是,两种执行方式,通过__name__获取的模块名称是不一致的。当模块由其他模块导入时,模块的名称就是文件的名称,而模块作为脚本执行时,模块的名称为__main__。因此,我们可以据此获取模块执行的方式。
11.3模块 搜索路径
当我们导入模块时,解释器会按照如下顺序搜索:
- 在解释器内建模块中搜索指定的模块名。例如,sys,math等。
- 作为脚本运行文件所在的路径。
- PYTHONPATH环境变量指定的路径。我们可以根据需要设置该路径,如果没有该环境变量,则忽略。
- 与Python安装相关的路径。这包含Python语言的内建模块(os, random模块),以及安装的第三方模块(beautifulsoup,numpy模块)所在的路径等。
我们可以通过sys模块的path属性获得路径信息。path的值(列表类型)就是由后三项按顺序组成的(后三项路径的并集)。解释器会在这些路径中寻找模块名.py文件。
解释器会按顺序进行查找,以先找到的为准。因此,如果在不同的路径中都存在满足条件的模块,会以先找到的为准。
11.4 模块 的缓存
当我们导入模块时,Python会缓存所加载模块编译后的版本。从实现的角度,就是将导入模块的文件(*.py)编程成字节码文件(*.pyc)。这样在下次运行时,就会检查字节码所对应的源文件在编译过后是否经过修改,即字节码文件是否是对应源文件的最新版本。如果不是最新版本,则会对源文件重新进行编译。如果已经是最新版本,则会从字节码文件中读取信息,这样可以加快模块的加载速度。
编译的字节码(*.pyc)文件是平台无关的,即不管在什么平台,只要Python源文件的内容相同,编译过后的字节码文件也相同。字节码文件会保存在与作为脚本运行文件相同路径下的__pycache__目录中。格式为“模块名.解释器-版本.pyc”,例如,假设导入的模块为test,解释器为CPython3.6,则字节码文件名为“test.cpython-36.pyc”。
说明:
- 缓存字节码文件只是可以提供模块加载的速度,并不会提高模块运行的速度。
- 只有导入的模块才会生成字节码文件,作为脚本运行的文件不会生成字节码文件。
- 字节码文件可以脱离源文件而运行。
11.5包
11.5.1 包 的概念
包类似于操作系统中的文件夹(路径),可以对一系列的模块按照层级结构进行组织。包具有以下作用:
- 包可以提供模块的分类管理。
- 包提供独立的命名空间,能够解决模块的命名冲突。
11.5.2导入包
在操作系统中,目录可以含有子目录。目录与子目录,目录与文件之间,使用特定的分隔符进行分隔。例如,Windows操作系统使用“\”,Linux系统使用“/”。同样,包也可以含有子包,包与包,包与模块之间使用“.”来分隔。
我们可以通过import来导入包,或者是导入包中的模块。与之前导入模块的语法是相同的,例如:
import 包名
import 包名.模块名
from 包名 import 模块名
from 包名.模块名 import 名称
刚才导入包的方式,我们称为绝对导入,此外,我们也可以进行相对导入。我们使用“.”来表示当前模块所在的包,使用“..”来表示当前模块所在包的父包(上级包)。例如:
from . import 名称
11.5.3__init__.py
在每一个包中,应该同时配有一个__init__.py文件(模块),作用如下:
- 该文件用来标记,当前的路径是一个包,而不是普通的目录。这样可以避免目录名与模块名造成混淆,影响在搜索路径上,后续模块的导入。
- 该文件为包的初始化模块,当导入包,或者导入包的子包(子模块)时,该模块会自动执行。因此,我们可以在__init__.py中编写一些包的初始化语句。
- 在__init__.py中定义的,具有全局作用域的名称,可以使用包名.名称进行访问(这些名称就会成为包(对象)的属性)。
接下来,我们就通过程序来对以上三点进行说明。
11.5.4__all__
我们可以在__init__.py中定义__all__变量,来控制导入哪些名称,这与模块中的定义的__all__变量意义相同,也是一个字符串的列表类型,指定能够导入的名称。
11.6数学模块
11.6.1 m ath
math模块提供了与数学计算相关的功能。常用的功能如下:
- pi:返回圆周率的值。
- e:返回数学常数。
- ceil(x):返回大于等于x的最小整数(向上取整)。
- floor(x):返回小于等于x的最大整数(向下取整)。
- exp(x):返回e的x次幂。相当于math.e ** x。
- pow(x, y):返回x的y次幂。相当于x ** y。
- log(x[, base]):返回基于base为底,x的对数。base默认为e。
- fabs(x):返回x(视为float类型)的绝对值。
- factorial(x):返回x的阶乘。x需要是int类型,或者是小数点为0的浮点类型,并且不能为负。
- fmod(x, y):返回x与y取余的结果。注意:x % y是取模,二者结果可能是不同的。
- fsum(iterable):返回可迭代对象中每个值累计求和后的结果。
- gcd(x, y):返回x与y的最大公约数。x与y需要是整数类型(greatest common divisor)。
- sqrt(x):返回x的平方根。
11.6.2random
random模块提供生成随机数的功能(伪随机数)。常用功能如下:
- random():返回一个0 ~ 1之间的浮点数,包括0,不包括1。
- randint(a, b):返回一个a ~ b之间的整数。包括a与b。
- randrange(stop) / randrange(start, stop[, step]):参数与range函数的意义相同,相当于从相同参数的range函数可能产生的值中,随便选择一个。
- uniform(a, b):返回a与b之间的浮点数,包括端点。
- choice(seq):从seq(可迭代对象)中随机选择一个元素。如果序列为空,则产生错误。
- choices(population, weights=None, *, cum_weights=None, k=1):从population(可迭代对象)中选择k个元素,放入一个列表并返回。如果提供了weights(权重)或cum_weights(累积权重),则元素的选择概率会根据权重(累积权重)决定。权重与累积权重不能同时指定,因为权重内部会转换成累积权重,这样会造成不一致。如果没有指定权重与累积权重,则各个元素的选择概率相同。
- sample(population, k):从population(可迭代对象)中选择k个不同的元素,返回元素组成的列表。
- shuffle(x[, random]):对x(序列)进行洗牌(随机分配每个元素的位置)。random是一个可选的生成随机数的函数,函数的返回值为[0, 1),默认使用random模块的random函数。
11.7日期 与时间
11.7.1 time
time模块提供关于时间的操作。
- timezone:时区,返回与UTC(Coordinated Universal Time)时间(世界标准时间)相差的秒数。正数表示晚于UTC时间,负数表示早于UTC时间。
- time():返回从从新纪元(1970-01-01 00:00:00)到当前时间走过的秒数。
- localtime([seconds]):返回从新纪元走过seconds(秒数)后的时间。返回类型为time.struct_time类型(tuple的子类),如果seconds没有指定,则默认表示当前时间。该时间显示的是本地时间,要考虑对应的时区。例如,北京时间为东八区,比UTC时间早8小时,因此,需要在最后转换的时间上加上8小时。struct_time是一个命名元组(可以通过属性名访问,也可以通过索引访问),包含如下几个部分:
|
|
|
0 |
tm_year |
年(四位数表示)。 |
1 |
tm_mon |
月份(1 ~ 12)。 |
2 |
tm_mday |
日(1 ~ 31)。 |
3 |
tm_hour |
小时(0 ~ 23)。 |
4 |
tm_min |
分(0 ~ 59)。 |
5 |
tm_sec |
秒(0 ~ 61),60用来表示闰秒(跳秒),是对世界时间的一种调整。61因为历史原因所保留。 |
6 |
tm_wday |
星期(0 ~ 6),周一为0,周日为6。 |
7 |
tm_yday |
年度的第几天(1 ~ 366)。 |
8 |
tm_isdst |
是否支持DST(daylight saving time),即夏令时(日光节约时间)。部分国家在天亮早的夏季,将时钟向前调整一小时,从而可以节约照明资源。可能值为0(不生效)、1(生效)或-1(未知)。 |
- gmtime([seconds]):与localtime([seconds])的用法相同,只是返回的是UTC时间,而不是本地时间。
- mktime(tuple):将tuple(本地时间的元组)转换为从新纪元到元组指定时间走过的秒数。该函数与localtime([seconds])函数正好是相反的。
- asctime([tuple]):将tuple(时间元组)转换成字符串(str)的表示形式。如果没有提供tuple参数,则使用localtime()函数返回的元组(当前的本地时间)。
- ctime([seconds]):将从新纪元走过的毫秒数转换为本地时间。该函数相当于这样调用:asctime(localtime(seconds))。如果seconds参数没有指定,则使用time()函数返回的秒数。
- sleep(seconds):使当前程序暂停执行参数指定的时间。seconds可以是小数。
- clock():在Unix / Linux系统,该函数返回CPU的计算时间。在Windows操作系统,该函数第一次调用,返回CPU的计算时间,从第二次调用开始,返回距离第一次调用该函数所经历的时间。CPU的计算时间不包括调用sleep暂停的时间,因为在暂停时,CPU没有工作。因为该函数在不同操作系统上行为的不一致性,从Python3.3起,已经不建议使用,取而代之的是使用perf_counter()函数或者是 process_time()函数。
- perf_counter():返回精准的性能计数器,可以用来测试短时间的时间差。该时间包含调用sleep函数暂停的时间。该函数返回值所基于的时间点是不确定的,我们不能当成系统时钟来使用,但是可以多次调用该函数,计算一段程序执行的时间差。
- process_time():返回当前进程下,系统以及用户的CPU计算时间。该时间不包含调用sleep函数暂停的时间。该函数返回值所基于的时间点是不确定的,我们不能当成系统时钟来使用,但是可以多次调用该函数,计算一段程序执行的时间差。
- strftime(format[, tuple]) -> string:将tuple(时间元组)转换成format参数指定的格式的字符串。如果tuple没有指定,则使用localtime()函数返回的元组。format中可以含有特殊占位符,将使用元组中特定值进行替换,非特殊占位符会原样显示。
|
|
%Y |
年份(四位数字)。 |
%y |
年份(两位数字)。 |
%m |
月份(01 ~ 12)。 |
%d |
日(01 ~ 31)。 |
%H |
24小时制(00 ~ 23) |
%I |
12小时制(00 ~ 12) |
%M |
分钟(00 ~ 59)。 |
%S |
秒(00 ~ 61)。 |
%w |
周期几(0 ~ 6),周日为0。注意,与tm_wday的表示不一致。 |
%j |
年度的第几天(001 ~ 366)。 |
%W |
年度的第几周(00 ~ 53),星期一视为一周的第一天。在年度第一个周一之前的天视为第0周。 |
%U |
年度的第几周(00 ~ 53),星期日视为一周的第一天。在年度第一个周日之前的天视为第0周。 |
%z |
当前时区与UTC的时间差。格式为+HHMM(当前时区早于UTC)或-HHMM(当前时区晚于UTC)。 |
%Z |
时区名称。 |
%a |
本地星期的简写名。 |
%A |
本地星期的全名。 |
%b |
本地月份的简写名。 |
%B |
本地月份的全名。 |
%c |
本地日期与时间的恰当表示。 |
%x |
本地日期的恰当表示。 |
%X |
本地时间的恰当表示。 |
%p |
显示AM或PM(或其本地等效的其他表示方式)。 |
%% |
%的转义。 |
- strptime(string, format) -> struct_time:按照format指定的格式,将string(时间格式字符串)解析为时间的元组。format的格式与strftime函数的format格式相同。
11.7.2datetime
datetime模块提供date,time,datetime,timedelta等类,供我们对日期与时间进行操作。与time模块相比,datetime模块还额外增加了日期的加减与比较运算。
date类提供针对日期(不含时间)的操作。
- date(year, month, day):用来创建参数指定日期的date对象。year指定年,month指定月,day指定日。
- year / month / day:返回年 / 月 / 日。
- max / min:最早的 / 最晚的date对象。
- resolution:类属性两个不同date对象的最小差距值。
- ctime():返回特定格式的字符串来表示日期。
- replace(year=self.year, month=self.month, day=self.day):返回新的date对象,值为使用当前参数替换之后的结果。year指定要替换的年,month指定要替换的月,day指定要替换的日。
- timetuple ():返回time.struct_time对象,类似于time.localtime()返回的结果。
- weekday():返回当前日期是星期几(0 ~ 6)。星期一返回0,星期日返回6。
- toordinal():返回当前日期的序数。1年1月1日序数为1,1年1月2日序数为2,以此类推。
- strftime(format):根据format指定的格式,显示当前的日期对象。
- today():类方法,返回当前的日期。
- fromtimestamp(timestamp):从参数指定的timestamp(时间戳,即从新纪元走过的秒数)中创建date对象。
- fromordinal(ordinal):根据参数指定的ordinal(序数)创建date对象。
time类提供针对时间的操作。
- time(hour=0, minute=0, second=0, microsecond=0):创建参数指定的time对象。hour指定小时,minute指定分钟,second指定秒,micorsecond指定微秒。
hour / minute / second / microsecond:返回小时 / 分钟 / 秒 / 微秒。
- max / min:最早的 / 最晚的time对象。
- resolution:类属性两个不同time对象的最小差距值。
- replace(hour=self.hour, minute=self.minute, second=self.second, microsecond=self.microsecond):创建一个新的time对象,值为参数指定的值替换之后的结果。hour指定要替换的小时,minute指定要替换的分钟,second指定要替换的秒,microsecond指定要替换的微秒。
- strftime(format):根据format指定的格式,显示当前的时间对象。
datetime类提供针对日期与时间的操作,相当于是date与time两个类功能的并集。
- datetime(year, month, day, hour=0, minute=0, second=0, microsecond=0):用来创建参数指定日期的datetime对象。year,month与day三个参数是必须的。
- year / month / day:返回年 / 月 / 日。
- hour / minute / second / microsecond:返回小时 / 分钟 / 秒 / 微秒。
- max / min:最早的 / 最晚的datetime对象。
- resolution:类属性两个不同datetime对象的最小差距值。
- date():返回date对象,年,月,日与datetime对象的年,月,日相同。
- time():返回time对象,时,分,秒,微秒与datetime对象的时,分,秒,微秒相同。
- ctime():返回特定格式的字符串来表示日期时间。
- replace(year=self.year, month=self.month, day=self.day, hour=self.hour, minute=self.minute, second=self.second, microsecond=self.microsecond):返回新的datetime对象,值为使用当前参数替换之后的结果。
- timetuple ():返回time.struct_time对象,类似于time.localtime()返回的结果。
- weekday():返回当前日期是星期几(0 ~ 6)。星期一返回0,星期日返回6。
- toordinal():返回当前日期的序数。1年1月1日序数为1,1年1月2日序数为2,以此类推。
- strftime(format):根据format指定的格式,显示当前的日期时间对象。
- today():返回当前的日期与时间。
- now():返回当前的日期与时间,与today方法类似。
- utcnow():返回当前的日期与时间转换为UTC之后的结果。
- fromtimestamp(timestamp):从参数指定的timestamp(时间戳,即从新纪元走过的秒数)中创建datetime对象。
- utcfromtimestamp(timestamp):从参数指定的timestamp(时间戳,即从新纪元走过的秒数)中创建UTC datetime对象。
- fromordinal(ordinal):根据参数指定的ordinal(序数)创建datetime对象。
- strptime(date_string, format):根据给定的date_string(日期时间字符串),按照format指定的格式进行解析,返回datetime对象。
11.8系统相关
11.8.1 sys
sys模块提供一些与解释器相关的功能。sys提供的属性很多,不过多数并不常用,我们这里仅介绍一些常见的属性。
- argv:返回一个列表,列表的第一个元素为命令行运行的文件名称,往后的每个元素为命令行在文件名称后面传递的每一个参数。当我们要进行一些全局性,系统相关的配置时,就可以使用命令行来传递参数。所传递的参数就可以通过argv来进行获取。
- version:返回Python的版本信息。
- copyright:返回Python的版权信息。
- path:返回模块的搜索路径。
- float_info:返回浮点类型(float)的相关信息。
- platform:返回操作系统信息。
- exit():退出Python解释器,终止程序的执行。
- getsizeof(object):返回object(对象)的大小(以字节为单位)。
- setrecursionlimit(n):设置最大递归的深度。
第十二章 异常
12.1. 异常说明
12.1.1. 异常概念
12.1.2. 常见异常类型
12.2. 捕获异常
12.2.1. try…except
12.2.2. 捕获多个异常
12.2.3. else
12.2.4. finally
12.3. 手动抛出异常
12.4. 自定义异常
在编程过程中,会出现各种“问题”,“问题”大致分为两种:
- 错误:在编译期出现的问题,不能被捕获
- 异常:在执行期出现的问题,可以被捕获
12.1异常说明
12.1.1异常 概念
异常是程序运行过程中产生的一种事件,该事件会打乱程序的正常流程。可以说,异常就是一种意外,指程序没有按照正常或期望的方式执行。
当异常产生时,会创建一个相关异常类的对象,该对象含有异常的相关信息。异常产生时,会在异常的上下文中寻找异常处理程序,如果没有异常处理程序,则异常产生之后的语句将不会得到执行。该异常会向上传播。传播的方式为:
- 如果异常在函数中产生,则会传播给函数的调用端。
- 如果异常在模块中(函数外)产生,则会传播给导入该模块的模块。
如果传播到作为脚本运行的模块,还未处理该异常,则会将异常传播给解释器,此时,整个线程终止执行。在控制台会打印出异常的相关信息与堆栈的调用轨迹。轨迹是按照方法调用的顺序或模块引用的顺序打印,离异常发生地最近的方法或模块会最后打印。
12.1.2 常见 异常类型
异常命名惯例,以Error结尾。
BaseException
Exception
- ZeroDivisionError
- NameError
- TypeError
- AtrributeError
- Indentation
- IndexError
- UnboundLocalError
- AssertionError
- ModuleNotFoundError
- KeyError
- RecursionError
- StopIteration
- ValueError
- SyntaxError
12.2捕获 异常
12.2.1 try …except
在Python中,可以使用try-except的语法来捕获异常,我们可以将其称为异常处理程序。格式如下:
try:
可能产生异常的程序
except 异常类型1:
恢复措施
except 异常类型2:
恢复措施
……
except 异常类型n:
恢复措施
其中,try用来执行可能会产生异常的程序,而except用来捕获try中产生的异常,用来执行一些恢复或补救操作,或者给出错误的提示信息等。这分为三种情况:
try语句块没有产生异常。
此时try语句块全部执行完毕,不会执行任何except分支(因为没有异常),然后继续执行try-except之后的语句。
try语句块中产生异常,except捕获了该异常。
此时会创建一个相关异常类的对象,异常之后的语句将不会得到执行。程序会跳转到except分支,从上到下依次使用异常类对象与每个except中的异常类型进行匹配(判断异常对象是否为except中异常类型的实例),哪一个except分支匹配成功,则执行哪个except分支,成功将异常捕获。try-except之后的语句正常执行。对于except分支,至多只会执行一个(执行先匹配成功的分支)。
try语句块中产生异常,但是except没有捕获该异常。
当try中产生异常时,try异常之后的语句不会得到执行。程序跳转到except分支进行异常匹配。没有匹配成功,则表示没有捕获,该异常继续存在,而try-except之后的语句不会得到执行,异常会继续向上传播(传播给函数的调用端或模块的引入处)。
我们可以使用except捕获异常,同时,我们也能够使用as语法获取try中产生的异常对象。语法格式为:
try:
可能产生异常的代码
except 异常类型 as 变量:
处理异常代码
当异常匹配成功时,我们就会将try中产生的异常对象赋值给as变量名,然后,我们就可以在except中使用变量名来访问异常对象了。
try:
print(5 / 0)
except ZeroDivisionError as e:
# 通过异常对象的args属性获取异常对象构造器的参数。
print(e.args)
异常对象的args属性返回一个元组类型,其中存放异常对象创建时,传递给构造器中的参数值。
12.2.2捕获 多个异常
因为在try语句块中,可能产生不止一种异常,故我们会使用多个except分支来捕获这些可能产生的异常。如果多个异常类之间没有继承关系时,except分支的顺序不是十分重要,但是,当异常类之间存在异常关系时,就一定要将子类放在前面,父类放在后面。因为子类异常对象也是父类异常类的实例,如果将父类分支放在子类分支之前,则就算try中产生子类异常,也会先由父类分支率先捕获,子类分支永远都没有机会执行,这就失去了意义。
所以,在捕获异常时,except分支应该按照从特殊性(子类)到一般性(父类)的方式排序。
except还有一种语法,就是不指定异常类型,此时表示捕获所有异常类型。例如:
try:
可能产生异常的代码
except: # 没有指定异常类型,会捕获所有的异常。
pass
因为捕获所有的异常类型是最广泛的(最具有一般性),所以,如果使用这种方式,则必须将该except置于最后(作为最后一条except分支)。
当try中产生了两种(或更多)的异常,而多种异常的处理方式又完全相同时,我们使用多条except分支,会造成代码的重复。
此时,我们可以使用一条except分支,同时捕获多个异常来代替。
try:
# 操作
except (IndexError, KeyError):
print("提供值不合法,获取失败!")
这样,无论try中产生IndexError还是KeyError,except分支都可以进行捕获。
也许大家会有这样的想法,这种捕获多个异常有什么用呢,使用不指定异常类型的except岂不是更好,能捕获所有异常,可谓“万事通用”。
try:
# 操作
except:
print("提供值不合法,获取失败!")
但是,如果这样做,就很可能会捕获预期之外的异常,从而掩埋真正的问题,令错误不易排查。
因此,如果能够捕获更加具体明确的异常类型,我们最好不要使用更加通用一般的异常类型代替。
12.2.3else
try-except还可以跟随可选的else语句。语法为:
try:
……
except 异常类型:
……
else:
……
当try中没有产生异常时,就会执行else分支,否则,不执行else分支。
12.2.4finally
try-except还可以带上一个可选的finally。如果同时存在else,则finally必须处于else的后面,其实,except,else,与finally都是可选的。但是,except与finally二者不能同时缺失,即二者至少要存在一个。
finally会在try-except-else之后得到执行(如果存在except或else的话),且一定会得到执行。即无论try是否产生异常,也无论except是否捕获try中产生的异常,finally终将会得到执行。
考虑到finally总是可以执行的特征,我们往往会在finally中执行一些清理的工作。例如,我们在try中申请了一些系统资源(文件读写,数据库连接等),就可以在finally中进行资源释放,从而不会造成资源泄露(资源申请,但没有释放)。如果将释放语句写在try中,则一旦在释放之前产生异常,则资源释放语句就不会得到执行。
finally是否真的总是会执行呢,我们现在做出如下的尝试:
- 在循环中,通过break尝试跳过finally语句块。
- 在方法中,执行return尝试跳过finally语句块。
- 调用sys模块的exit方法,尝试跳过finally语句块。
从而发现,finally语句块确实总是会得到执行。在try中使用break、return或其他可能会跳过finally语句体的语法时,程序会检测当前是否存在finally,如果存在,则会首先执行finally语句体,然后才能放心的跳出或结束程序。
既然如此,如果在某函数中,try中使用了return语句,返回一个值,而finally中同时使用了return语句,返回了另外一个值,此时,函数调用端接收到的,是哪个值呢?
但是,当try语句体中尝试返回一个变量时,如果在finally中去修改该变量的值,不会影响到返回值的结果(返回的还是修改之前的值)。
12.3手动 抛出异常
我们也可以自行创建一个异常类型的对象,然后将其抛出。这与之前产生异常的行为是相同的。也许大家为问:异常是一种意外,是我们应该极力去避免的,为什么还要主动去“创建”异常呢?
我们在编写程序时,可能会接收调用端传递过来的值,但是,我们无法保证调用端传递的值永远是正确的。例如,在注册用户的时候,提供年龄信息,如果输入了负值,这明显是不正确的。我们可以使用if来进行判断:
def register(age):
if age > 0:
注册操作
我们进行了合理的判断,但问题是,如果age小于等于0,则程序会“保持沉默”,没有任何信息提示。这对于调用端来说,可能未必是一件好事。如果这个行为很重要,那此时产生一个异常才是更合理的结果。因为使用if判断的形式,纵然调用端传递非法的值,我们最多也只是不执行操作,但却没有什么有效的措施能够牵制调用端,或是以一种强力的方式去通知调用端已经犯了较为严重的错误。尽管,我们可以使用print函数打印提示信息,但调用端很可能会忽略这些信息。
相反,抛出异常的方式则不同。异常可以向上传播,如果调用端没有明确对异常进行处理,将会导致当前的线程终止,同时显示异常的错误信息,这就可以引起调用端足够的重视,而不至于掩盖程序的bug(软件漏洞)。
我们可以创建一个异常对象,使用raise抛出,语法为:
raise 异常类或异常对象
例如:
raise Exception("产生异常")
raise Exception
raise后面跟随异常类,则相当于是调用其无参的构造器,因此,后者相当于:
raise Exception()
raise后面必须是一个有效的异常类(BaseException类型或其子类型)或对应异常类的对象,如果是其他类型,将会产生错误。
这里还有一个问题,既然调用端可以采用try-except处理异常,那为什么不在register方法里面捕获可能的异常呢?这样不就方便所有的调用端了吗?原因如下:
- 如果在方法内捕获异常,则异常消失,就不能给调用端一个有效的提醒。
- 如果在方法内捕获异常,则需要对异常产生时做出处理。
12.4自定义 异常
之前,我们使用的是内建的异常类型ValueError,但是,ValueError是Python内建的异常类型,其具有自身特殊的应用场景,如果我们使用系统内建的异常类型,容易造成混淆。例如,我们在年龄不合法时抛出该异常,而数值转换失败时,移位运算右侧操作数为负数时也会产生该异常,这就不便于我们定位与排查问题。因此,我们可以自定义异常类型,用在我们需要的场景,这样就可以避免与内建异常类型相互干扰。
自定义异常通常继承Exception类型(或其子类型),按照惯例,异常类以Error结尾。现在,我们就将之前的程序改写,自定义一个异常类型。
第十三章 迭代器、生成器、装饰器
13.1. 迭代
13.1.1. 可迭代对象与迭代器
13.1.2. 自定义迭代类型
13.1.3. 迭代合体
13.2. 生成器
13.2.1. 需求背景
13.2.2. 生成器表达式
13.2.3. 生成器函数
13.3. 装饰器
13.3.1. 闭包
13.3.2. 需求背景
13.3.3. 使用装饰器
13.3.4. 含有参数的装饰器
13.3.5. 保留函数信息
13.3.6. 类装饰器
13.1迭代
13.1.1 可迭代对象与迭代器
我们之前学习过序列,字典与集合类型。这些类型都可以看做是一个容器,用来存放多个元素,并且每种类型都提供了相应的方法,来操作容器中的元素。这些类型都可以用在for循环中进行遍历,依次获取容器中的每一个元素。从简单的角度讲,这些可以用在for循环中,进行遍历的对象,我们称其为可迭代对象。
可迭代对象类型在collections.abc.Iterable类中定义,因此,我们往往可以通过某对象是否为Iterable实例的方式,来判断该对象是否为可迭代对象。
Iterable类是一个抽象基类(父类),用来定义可迭代对象的规范,即可迭代对象应该具有的公共特征。该接口中定义了一个用来表示规范的方法(抽象方法):
def __iter__(self)
该方法用来返回一个迭代器,用来依次访问容器中的数据。所谓迭代器,就是一个数据流对象,可以连续返回流中数据。可以说,可迭代对象能够在for循环中遍历,底层靠的就是迭代器来实现的。
对于迭代器类型,是在collections.abc.Iterator中定义。该类型也是一个抽象基类,用来定义迭代器对象的规范,Iterator继承Iterable类型, 两个重要的方法如下:
def __next__(self)
返回下一个元素,当没有元素时,产生StopIteration异常。
def __iter__(self)
从父类Iterable继承的方法,意义与Iterable类中的__iter__方法相同,即返回一个迭代器。因为当前对象就是迭代器对象,所以在该方法中,只需要简单的返回当前对象即可:
return self
正规来说,作为可迭代对象,需