目录
第十一章 模块和包
11.1模块介绍
模块是一个Python文件,该文件包含相关的定义与语句(类,函数,变量)。模块具有名称,名称与文件的名称一致。
模块具有如下的好处:
- 通过划分若干个模块,我们就可以将项目程序进行明确的划分,从而将复杂问题简单化,同时,也能够进行更加合理的分工,有利于协作式开发程序。
- 模块提供独立的命名空间,可以解决命名上的冲突。不同的模块中,定义相同名称的变量,不会产生命名冲突。
- 模块可以实现良好的重用性,在多人间实现共享。
11.2模块的使用
大的项目,可以划分为多个模块。但模块与模块之间,不可能都是完全孤立的。一个模块很可能需要与其他的一个(或多个)模块进行交互,因此,如果我们需要在一个模块中使用其他模块定义的名称(函数,类,变量等),则需要首先导入该模块,然后通过模块名.名称进行访问。
11.2.1导入模块
我们可以使用import对模块进行导入:
import 模块
我们还可以一次导入多个模块:
import 模块1,模块2……
当导入模块时,该模块内的语句就会得到执行,但只会执行一次,即重复导入模块不会多次执行。当模块作为脚本执行时(使用python命令在命令行执行),模块中的语句也会得到执行。按照惯例,模块导入语句写在模块的最上方。
当导入模块后,我们就可以使用模块中所定义的名称,格式为:
模块名.名称
模块的访问
我们也可以将其他模块中定义的名称(全局命名空间中的名称)直接导入到当前模块的命名空间,这样,我们就可以直接通过名称访问,而无需使用模块名限定。格式如下:
from 模块 import 名称1,名称2……
不过,此种导入方式要稍加留意,因为如果当前模块中也存在同样名称的定义,就会造成名称的冲突,也就是名称会重新绑定后来的对象。
名称的冲突
假设我们现在不考虑名称的冲突,只考虑访问的便捷性,使用from import的方式确实是不错的选择。但是,如果我们要使用该模块中定义的很多名称,一个个的导入可能会有些繁琐,此时,我们可以使用批量导入:
from 模块 import *
这样就会将模块中除了以_开头的所有名称导入到当前模块的命名空间。然而,这种方式会导入很多名称,容易造成命名冲突,尽可能少用。
dir函数
- dir([object])
11.2.2模块别名
当导入模块(或模块中的名称)时,我们可以使用as为模块(或模块中的名称)指定别名。语法如下:
import 模块名 as 模块别名
from 模块名 import 名称 as 名称别名
这样,我们就可以通过别名来访问模块(或模块中的名称)。但是,一旦指定别名后,原模块名(或原模块中的名称)将不再可用。
指定别名具有如下好处:
- 在使用import 或 from时,如果产生命名冲突,可以使用别名来解决。
- 如果模块或名称较长,可以使用简短的别名,减少输入量。
11.2.3隐藏模块数据
因为使用import *的语法会在当前命名空间增加很多名称,为了减少import *所造成的影响(名称冲突),我们可以有选择性的隐藏模块的数据,进而限制使用import *时,名称的导入。
隐藏模块数据可以采用两种方式:
- 将名称以下划线(_)开头。
- 定义__all__变量。
说明
- 以上两种隐藏方式仅是限制使用import *语法导入的名称,并不是表示该名称无法在模块外访问。使用import其他方式导入,还是能够在模块外进行访问的。
以下划线(_)开头定义名称
如果模块中定义的名称以_开头,则在使用import *语法时,这些名称不会导入到当前模块的命名空间中,即当前模块无法直接访问该名称。
__all__
如果使用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文件。
查看sys.path的值。
解释器会按顺序进行查找,以先找到的为准。因此,如果在不同的路径中都存在满足条件的模块,会以先找到的为准。
模块的选择
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.1math
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.1time
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是一个命名元组(可以通过属性名访问,也可以通过索引访问),包含如下几个部分:
表格 131 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中可以含有特殊占位符,将使用元组中特定值进行替换,非特殊占位符会原样显示。
表格 132 占位符
占位符 | 说明 |
%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类提供针对日期(不含时间)的操作。
构造器
- 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类提供针对时间的操作。
构造器
- 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类
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.1sys
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常见异常类型
异常命名惯例,以Error结尾。
BaseException
Exception
- ZeroDivisionError
- NameError
- TypeError
- AtrributeError
- Indentation
- IndexError
- UnboundLocalError
- AssertionError
- ModuleNotFoundError
- KeyError
- RecursionError
- StopIteration
- ValueError
- SyntaxError
12.2捕获异常
12.2.1try…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总是会执行的尝试
finally是否真的总是会执行呢,我们现在做出如下的尝试:
- 在循环中,通过break尝试跳过finally语句块。
- 在方法中,执行return尝试跳过finally语句块。
- 调用sys模块的exit方法,尝试跳过finally语句块。
从而发现,finally语句块确实总是会得到执行。在try中使用break、return或其他可能会跳过finally语句体的语法时,程序会检测当前是否存在finally,如果存在,则会首先执行finally语句体,然后才能放心的跳出或结束程序。
try与finally中的return
既然如此,如果在某函数中,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可迭代对象与迭代器
我们之前学习过序列,字典与集合类型。这些类型都可以看做是一个容器,用来存放多个元素,并且每种类型都提供了相应的方法,来操作容器中的元素。这些类型都可以用在for循环中进行遍历,依次获取容器中的每一个元素。从简单的角度讲,这些可以用在for循环中,进行遍历的对象,我们称其为可迭代对象。
可迭代对象类型在collections.abc.Iterable类中定义,因此,我们往往可以通过某对象是否为Iterable实例的方式,来判断该对象是否为可迭代对象。
回忆:之前学习的类型,哪些可以用在for循环当中?
判断序列,字典,集合与数值类型是否为可迭代对象。
Iterable类是一个抽象基类(父类),用来定义可迭代对象的规范,即可迭代对象应该具有的公共特征。该接口中定义了一个用来表示规范的方法(抽象方法):
def __iter__(self)
该方法用来返回一个迭代器,用来依次访问容器中的数据。所谓迭代器,就是一个数据流对象,可以连续返回流中数据。可以说,可迭代对象能够在for循环中遍历,底层靠的就是迭代器来实现的。
对于迭代器类型,是在collections.abc.Iterator中定义。该类型也是一个抽象基类,用来定义迭代器对象的规范,Iterator继承Iterable类型, 两个重要的方法如下:
def __next__(self)
返回下一个元素,当没有元素时,产生StopIteration异常。
def __iter__(self)
从父类Iterable继承的方法,意义与Iterable类中的__iter__方法相同,即返回一个迭代器。因为当前对象就是迭代器对象,所以在该方法中,只需要简单的返回当前对象即可:
return self
正规来说,作为可迭代对象,需要实现__iter__方法或者__getitem__方法。
def __iter__(self)
该方法用来返回一个迭代器,用来遍历容器中的元素。
def __getitem__(self, key)
该方法用来返回self[key]的结果。
- 说明:可迭代对象也可以不实现__iter__方法,而是实现__getitem__方法,这里大家可能会造成困扰。因为如果这样做,就与抽象基类Iterable中定义的行为不一致(Iterable中仅定义了__iter__方法)。实际上,这是由于历史原因造成的,保留__getitem__方法是为了做到兼容以前的实现。
判断类型是否为迭代器类型
for循环内部的工作方式
在使用for循环来遍历容器中的元素时,底层会调用iter函数,来返回容器的迭代器。iter函数首先检查类是否实现__iter__方法,如果实现,则调用该方法,返回迭代器。否则,会创建一个迭代器,然后调用__getitem__方法依次获取元素。如果以上两个方法都不存在,则表示当前对象并不是一个可迭代对象,因此也就不能放在for循环中使用(产生错误)。
在遍历容器中的元素时,会调用迭代器的__next__方法,返回下一个元素,如此反复执行。当没有可用的元素时,迭代器会产生StopIteration异常,而这个异常,会由for循环内部进行捕获,无需我们显式处理。
还原for循环本身
一次性的迭代器
对于迭代器,如果我们只想获得下一个元素而不是遍历,可以调用__next__方法而实现,不过,我们往往不会直接调用Python中的特殊方法,内建函数next可以帮助我们获取迭代器的下一个元素,next在内部会调用迭代器的__next__方法。同时,我们需要注意,迭代器只能迭代一轮,也就是说,如果容器中已经没有可用的元素,则迭代器就不能再次使用了(再次调用next函数获取下一个元素会产生异常),如果想要重新进行迭代,需要再次调用iter函数获取一个新的迭代器对象。
13.1.2自定义迭代类型
除了Python语言提供的内建可迭代类型外,我们也可以自定义迭代类型。不过,自定义的迭代类型也可以不继承Iterable或者Iterator,只需要满足抽象基类定义的规范,这样的类型就会成为Iterable或者Iterator的子类型。即:如果自定义可迭代对象类型,需要实现__iter__方法,如果自定义迭代器,除了实现父类中的__iter__方法外,额外实现__next__方法。
13.1.3迭代合体
在上例中,我们在可迭代对象中返回迭代器,但是我们不禁又产生了这样的设想:为什么Python在实现序列等类型时,将其设计为可迭代对象,但是却不是迭代器呢?或者说,我们是否可以将二者何为一体,让序列等类型作为可迭代对象的同时,也是一个迭代器类型呢?如果二者可以合体,上例的程序就可以不用再定义一个新的迭代器类,直接在可迭代对象的__iter__方法中返回自身(self),然后同时实现__next__方法不就可以了吗?
尝试合体。
13.2生成器
13.2.1需求背景
假设我们有如下的需求:计算并能够返回1 ~ 100内所有自然数的平方。根据以前所学习的知识,我们可以轻松实现:
def compute():
result = []
for i in range(1, 101):
result.append(i * i)
return result
再简单一点,我们可以通过列表推导式来实现:
def compute():
return [i * i for i in range(1, 101)]
这样做是没有问题的,但是,我们现在将问题升级,如果要计算的数值不是100个,而是一个海量甚至无限的数据集(例如,全体正整数的平方值),我们将大量的计算结果都存储一个列表当中,这就会占用大量的内存空间,导致程序运行非常缓慢。
Python语言中提供的生成器就类似于上述的处理方式,顾名思义,生成器类似于生产数据的工厂。在工作方式上,生成器不会预先准备好所有的数据,而是在需要时,每次仅生成一个数据。这样,在处理大量数据时,也不会占用大量的内容空间。我们可以使用两种方式来创建生成器:
- 生成器表达式
- 生成器函数
13.2.2生成器表达式
生成器表达式的语法非常简单,只需要将列表推导式的中括号改成小括号就可以了。我们还是以之前求平方值的程序为例。
生成器与迭代器有什么关系?
13.2.3生成器函数
当我们需要的数据集计算比较简单时,使用生成器表达式是一个不错的选择。但如果数据集的计算方式较为复杂,我们也可以使用生成器函数来实现。在生成器函数中,我们使用yield关键字来生成一个值,并将该值返回给生成器的调用端,格式为:
yield [生成的值]
这种语法,我们称为yield表达式。其中,生成的值是可选的。
程序:使用生成器函数,来产生斐波那契数列。
生成器函数与普通的函数非常相似,从形式上,只是使用yield代替了return而已。不过,我们不要小看yield关键字,如果函数中出现该关键字,则表示该函数为生成器函数,与普通的函数还是有很大差异的,说明如下:
- 生成器函数更像是一个类的定义,而不是函数的定义。因此,在第11行语句处,我们是创建了一个生成器的对象,而不是调用函数,所以,此时生成器函数体不会执行,如果是普通函数,则会执行函数体。
- 当调用生成器的方法时,生成器函数体才会执行。方法调用可以是显式调用:
# 假设gen为调用生成器函数而返回的生成器对象。
gen.__next__()
当然,也可以是隐式调用。例如,在for循环,调用内建函数next等场合时,也会隐式调用生成器函数的__next__方法。在上例中,for循环内会隐式调用生成器的__next__方法(第12行)。当遇到yield表达式时,会暂停执行,并将yield生成的值返回给生成器的调用端。同时,生成器函数会保存当前的运行时环境,局部变量等信息都会保留,不会丢失。如果是普通函数,则函数内定义的局部变量值会丢失,每次调用函数,局部变量都会具有新的初始值。
- 当再次调用生成器方法时,会在上次yield暂停的位置处,继续执行,直到遇到下一个yield表达式,产生下一个值,或者遇到return语句,产生异常而结束。如果生成器函数中没有return语句,则函数体执行结束,也会隐式执行return返回。
yield表达式
当调用生成器方法,生成器首次执行时,会在yield处暂停,然后将yield生成的值返回至生成器调用端。当再次调用生成器的方法,就会从之前处于暂停的位置处(即yield所在的位置)恢复执行,此时,yield表达式就会获得值,具体的值由生成器调用端传递。如果调用的是send方法,则yield表达式的值为send方法调用时所传递的实际参数值,如果调用的是__next__方法,则yield表达式的值为None。
如果在生成器函数第一次执行前,调用send方法,则send方法的参数必须为None。因为生成器函数体在第一次时执行时,会暂停在yield位置,yield表达式尚未获得任何值,直到第二次执行生成器函数,yield表达式才会获得值,因此,第一次执行时,send方法传递任何数值都没有意义。
通过send方法,我们就可以在生成器调用端向生成器传递值,同时,生成器调用端又可以获取生成器中使用yield所生成的值,从而实现二者之间的数据关联。
13.3装饰器
13.3.1闭包
闭包是指在函数体内部,访问到其外围函数中定义的变量。则对于内部函数,我们就称为闭包。从定义可知,闭包是发生在函数嵌套的上下文环境中。从代码实现的角度,我们通常会定义嵌套函数,然后将内部函数作为外围函数的返回值,返回给函数的调用端,供调用端多次调用执行。
闭包的优点就在于:
- 其访问外围函数中定义的变量,不会随着内部函数的执行结束而销毁,当我们下次再次执行内部函数时,所引用的外围函数中定义的变量依然会保留上次的值。这样,函数执行时的状态就得以保存,以便于调用端可以多次使用。
- 内部函数定义在外围函数中,作为外围函数的返回值。因此,内部函数名称就是一个局部名称(定义在局部命名空间中),不会被外界直接访问,这样就不会对外围函数的外部带来命名冲突等影响,当内部函数名称发生改动时,也不会带来任何问题,从而具有封装的特征。
现在我们来实现上班的函数,函数的功能是能够记录员工是第几次上班。如果使用以前函数的形式,则无法实现我们的需求,因为函数内定义的变量(状态)无法在函数调用后得到保存。
其实,我们也可以使用定义类的形式,来实现闭包的效果。
13.3.2需求背景
我们以员工上班与下班为例。
需求变更,要求在上下班增加签到日期与时间。
进行修改。
13.3.3使用装饰器
装饰器,用来处理被其所装饰的函数,然后将该函数返回。从实现的角度讲,装饰器本身也是一个函数,其参数用来接收另外一个函数,然后返回一个函数,返回的函数与参数接收的函数可以相同,也可以不同。
def decorator(函数1):
return 函数2
其实装饰器使用的,就是闭包的思想。当定义好一个装饰器(函数)后,就可以使用如下的语法来修饰另外一个函数:
# decorator为装饰器函数名。
@decorator
def fun():
pass
经过这样修饰后,在任意调用fun函数的位置:
fun()
就相当于执行:
fun = decorator(fun)
这与我们之前使用闭包的形式是一样的。我们将这种使用@修饰函数的形式称为装饰器的语法糖(syntactic sugar)。所谓语法糖,就是某种特殊的语法,用来方便程序员使用。
- 说明:装饰器本身除了是函数以外,也可以是一个类。我们在后面说明。
装饰器的优势在于:我们可以在不修改现有函数的基础上,对其进行的扩展,增加额外的功能。一个装饰器可以用来修饰多个函数,这样,我们就可以避免代码的重复,有利于程序的维护。
装饰器优化
我们还可以对之前实现的装饰器进行如下的优化:
- 装饰器使用参数接收的函数,可能是具有返回值的,因此,我们在内部函数中,不应该仅仅只是调用参数接收的函数,还应该将参数接收函数的返回值作为内部函数的返回值而返回。
- 装饰器可能会用来修饰很多函数(对很多函数进行功能扩展),例如,对上班与下班,开会与散会等很多函数记录时间。但是,每个函数定义的参数数量可能不尽相同,例如,我们当前的上下班函数定义了一个参数,但是,以后新增的开会与散会函数可能会定义两个或更多参数,也可能没有参数。
叠加装饰器
在开发项目时,因为需求的不确定性,业务的不断发展,功能的不断扩充等诸多原因,我们很难做到一步到位,因此,我们可能对现有功能不止一次的进行扩展。此时,我们可以对装饰器进行叠加,以便于对功能进行多次扩展。格式如下:
@decorator1
@decorator2
def fun():
pass
叠加修饰后,就相当于执行:
fun = decorator2(fun)
fun = decorator1(fun)
目前需求再次变更,公司老板决定,不仅要求员工上下班进行签到,而且还要在每天上班后,对今天的工作计划进行简要的汇报。
13.3.4含有参数的装饰器
我们使用装饰器完美的实现了需求。但是,输出的结果含有微秒,这可能并不是所有人都想要的。对于签到时间,我们只需要精确到分钟就够用了。因此,我们提供一种功能:让客户端指定输出格式。
如果让客户端指定输出格式,我们应当通过参数传递到装饰器中,然而,装饰器的参数已经预定好了,是用来接收我们使用@修饰的函数。因此,我们只能另辟蹊径。
方法就是,我们再定义一层函数,用来接收装饰器的参数,然后返回装饰器。这样,返回的装饰器就会停留在我们需要修饰的函数上,继续修饰对应的函数。
13.3.5保留函数信息
我们可以使用functools.wraps来解决,wraps接收一个函数,可以将接收函数的元信息复制到其所修饰的函数中。
13.3.6类装饰器
我们在使用闭包时,也介绍了使用定义类来实现同样功能的方式。既然装饰器使用的是闭包的思想,那装饰器是否也可以使用类的方式来实现呢?
实际上,装饰器不仅可以是函数,只要是可调用的对象,就能够成为装饰器。在Python中,类也是对象(一切皆为对象)。而调用类时,返回的其实就是类所创建的对象。因此,类也可以成为装饰器。
第十四章 文件
14.1读写文件
14.1.1获取文件对象
文件的概念,我们都不会感到陌生。在操作系统中,一个word文档,一张图片,一首音乐,这些都是文件。文件会以其固有的格式保存在硬盘中。文件可以分为两种类型:
- 文本文件
- 二进制文件
其中,文本文件由若干可识别的字符组成,并且不能包含非文本字符之外的内容(图片等),例如,.txt,.bat都是文本文件,而.doc,.pdf则不是文本文件。二进制文件则不是由可识别的字符组成,如果我们用文本编辑器打开二进制文件,往往看到的都是一堆乱码。其实,计算机只支持二进制,所以,从底层的角度来说,一切都是二进制格式的,文本文件,也是二进制文件的一种,只是其内容,是我们能够识别的字符而已。
在Python中,我们可以通过open函数返回文件对象。这里的文件对象是一个泛指,其不只可以表示文件,也可以表示文件夹(路径)。格式如下:
open(file, mode='r')
file指定文件的路径,可以是相对路径,也可以是绝对路径,mode指定打开文件的模式,如下表。当读入文件或者写入文件时,会涉及到文件指针。文件指针指向的就是下一次要读取或写入的字符(或字节)位置,随着读取或写入的进行,文件指针也会随之而移动。
表格 151 模式说明
模式 | 说明 |
r(1) | 读模式(默认模式),用来读取文件内容。文件指针在文件的开头。文件需要事先存在,否则会产生异常。 |
w(1) | 写模式,用来向文件写入数据。文件指针在文件的开头。如果文件存在,则覆盖文件,否则会创建文件。 |
a(1) | 追加模式,用来向文件追加数据。文件指针在文件的末尾。如果文件存在,不会覆盖文件(追加写入),否则会创建文件。 |
x(1) | 写模式,用来向文件写入数据。文件指针在文件的开头。如果文件不存在,创建文件,否则产生错误。 |
t(2) | 文本(字符串)模式(默认模式),以文本形式操作文件。 |
b(2) | 二进制(字节)模式,以二进制形式操作文件。 |
U(3) | 通用换行符模式(已不建议使用)。在该模式下,\n,\r\n或\r都会解析为换行符。不能与w、a、x或+模式同时使用(仅限于读取文件时使用)。 |
+(3) | 读取或写入。 |
在上表的(1)(2)(3)模式中,不同组的模式可同时使用(除了与U不兼容的模式),例如rt,wb+等。同一组的模式同时使用,例如rw,tb等。
练习:在文件使用期间,我们尝试去修改文件的名字(或者删除文件),能够成功吗?
关闭文件
当文件不再使用时,我们需要对文件进行关闭,从而断开与外部文件的连接。断开连接可以调用文件对象的close方法。
思考:应该在哪里调用close呢?
直接关闭。
在finally中关闭。
使用with关闭。
14.1.2文件的读取
我们可采用如下方法来读取文件:
- read(size=-1)
读取并返回文件内容。size指定读取的大小。如果是文本模式,以字符为单位,如果是二进制模式,以字节为单位。如果size省略,或者为负数,则返回文件的全部内容。如果文件已经没有内容可读取,返回空串(""或b"")。
- readline()
返回文件的一行,保留末尾的换行符。如果没有内容可以读取,返回空串(""或b"")。
- readlines()
返回一个列表,列表中的每个元素为文件的一行内容,每行保留末尾的换行符。
文件对象也是迭代器
然而,如果文件过大,这会占据大量的内容空间。此时readlines不是一个好的选择。对于文件对象,其本身也是一个迭代器类型,我们可以使用for循环的方式对文件对象进行迭代,从而节省内存。
程序:文件迭代。
14.1.3文件的写入
我们可以使用如下方法向文件写入数据:
- write(content)
将content参数写入到文件中,返回写入的长度。如果是文本模式,以字符为单位,如果是二进制模式,以字节为单位。
- writelines(lines)
参数lines为列表类型,将列表中所有元素写入文件中。
写入文件。
练习:实现文件的复制。
14.1.4文件定位
我们可以调用文件对象的如下方法,获取或设置文件指针的位置:
- tell()
返回文件指针的位置,即下一个要读取或写入的字符(字节)位置。以字节为单位。
- seek(offset, whence)
改变文件的指针。offset指定新的索引位置偏移量。whence指定偏移量的参照位置:
- 0:从文件头计算
- 1:从当前位置计算
- 2:从文件末尾计算
14.2文件与路径的操作
14.2.1os模块
os模块提供了很多操作目录与文件的功能。
- mkdir(path)
创建path指定的目录。如果path所在的父目录不存在,或者path目录已经存在,都会产生异常。
- makedirs(path, exist_ok=False)
创建path指定的目录。如果path所在的父目录不存在,则会连同父目录一同创建。如果path目录已经存在,当exist_ok值为False,会产生异常,如果exist_ok值为True,则不会产生异常(默认值为False)。
- rmdir(path)
删除path指定的空目录,但不会删除父目录。如果path目录不存在,或者该目录不为空,将会产生异常。
- removedirs(path)
删除path指定的空目录。如果父目录也为空,则会连同父目录一同删除(一直到根目录为止)。如果path不存在,或者该目录不为空,将会产生异常。
- remove(path)
删除path指定的文件。如果path不是一个文件,或者文件不存在,将会产生异常。
- rename(src, dst)
重命名一个文件或目录。src指定源文件的路径,dst指定重命名后的文件路径。src与dest要求为同一目录。
- renames(old, new)
与rename相似,但是old与new指定的目录可以不同(此时类似于移动文件)。在方法执行时,会删除old路径中左侧所有的非空目录,并根据需要,创建new路径中不存在的目录。在Windows系统,old与new必须在同一盘符中。
- getcwd()
返回当前的工作目录,即以脚本运行文件所在的目录。
- os.walk(path)
遍历路径下的文件
14.2.2os.path模块
os.path模块提供了关于路径操作的相关功能。
- abspath(path)
返回path的绝对路径。
- basename(path)
返回path的最后一个部分。即path中操作系统分隔符(/或\等)最后一次出现位置后面的内容。如果path以操作系统分隔符结尾,则返回空字符串。
- commonpath(paths)
参数paths为路径的序列,返回最长的公共子路径。
- dirname(path)
返回path的目录部分。
- exists(path)
判断路径是否存在,存在返回True,否则返回False。
- getatime(path)
返回文件或目录的最后访问时间。
- getmtime(path)
返回文件或目录的最后修改时间。
- getsize(path)
返回文件的大小,以字节为单位。
- isdir(path)
判断path是否为存在的目录,是返回True,否则返回False。
- isfile(path)
判断path是否为存在的文件,是返回True,否则返回False。
- join(path, *paths)
连接所有的path,以当前操作系统的分隔符分隔,并返回。空path(除了最后一个)将会丢弃。如果最后一个path为空,则以分隔符作为结尾。如果其中的一个path为绝对路径,则绝对路径之前的路径都会丢弃,从绝对路径处开始连接。
- split(path)
将path分割成一个元组,元组含有两个元素,第2个元素为path的最后一个部分,第一个元素为剩余之前的部分。(dirname与basename)
14.2.3shutil模块
shutil模块提供了高级操作文件的方式。我们可以通过该模块提供的功能,快捷方便的对文件或目录执行复制,移动等操作。
- copy(src, dst)
复制文件,返回复制后的文件路径。src指定要复制的文件,如果dst是一个存在的目录,则将文件复制到该目录中,文件名与src指定的文件名一致,否则,将src复制到dst指定的路径中,文件名为dst中指定的文件名。
- copy2(src, dst)
与copy函数类似,但是copy函数不会保留文件的元信息,例如创建时间,最后修改时间等。copy2函数会尽可能保留文件的元信息。
- copytree(src, dst)
复制一个目录,目录中的文件与子目录也会递归实现复制,返回复制后的目录路径。src指定要复制的目录,dst指定复制后的目标目录,如果dst已经存在,则会产生异常。
使用该函数复制目录时,可以结合ignore_patterns函数对目录下的文件与子目录进行排除。
例如:
- shutil.copytree("abc", "def", ignore=shutil.ignore_patterns("*.txt"))
ignore参数指定忽略的文件或目录,这样,所有名称以txt结尾的文件或目录将不会复制到目标目录中。
- shutil.copytree("abc", "def", ignore=shutil.ignore_patterns("a*"))
参数a*指定所有以a开头的文件或目录不会将不会复制到目标目录中。
- rmtree(path)
删除path指定的目录,目录中的子目录与文件也会一并删除。
- move(src, dst)
将文件或目录移动到另外一个位置,src指定文件或目录的路径,当src为目录时,会将该目录下所有的文件与子目录一同移动(递归)。dst指定移动的目标文件或目录。
14.3序列化
在我们开发过程中,我们会读取或设置配置信息,对数据进行分析,将爬取的数据保存等……因此,我们可能会操作不同类型的文件。本节会介绍csv与json文件类型的特征,然后给出该类型文件的读取与写入方式。
14.3.1csv
CSV(Comma Separated Values),是一种存文本格式的文件,文件中各个元素值通常使用逗号(,)进行分隔,但这不是必须的,扩展名为.csv。例如,一个班级的学生信息,我们就可以存储为csv的格式:
姓名,年龄,所在小组
张某,15,A组
赵某,14,B组
王某,15,C组
……
我们可以使用csv模块来操作csv类型的文件。
14.3.2json
JSON(JavaScript Object Notation),是一种轻量级的数据交换格式。json采用的是一组键与值的映射,键与值之间使用“:”进行分隔,而键值对之间使用“,”进行分隔。json文件中的类型可以是:
- 对象类型:使用{}表示。
- 数组类型:使用[]表示。
- 字符串类型:使用双引号界定。
- 布尔类型:true与false。
- 数值类型:整数与浮点数。例如5,2.8等。
json格式的文件示例如下:
{
"bg": "green",
"title": {
"data": ["data1", "data2", "data3", "data4"],
"align": "左对齐"
}
}
从json文件的文件格式我们发现,这非常类似于Python中的字典类型。的确,我们可以在json文件与Python中的字典类型之间进行转换。json模块提供了相关的功能。
处理程序【dump&load】
使用json格式的文件可以方便的进行数据交换。我们可以通过json类型的数据进行对象的序列化与反序列化。所谓序列化,就是将对象类型转换成字符串的形式。而反序列化,即为将序列化的字符串恢复为对象类型。通过序列化与反序列化,我们就可以方便的对复杂的对象进行存储与恢复(因为文件读写只支持字符串类型),或者通过网络进行传输,将对象共享给远程的其他程序使用。
序列化与反序列化程序【dumps & loads】
因为Python中的数据类型与json格式的数据类型并非完全相符,因此,在进行转换的时候,可能会进行一些映射处理,如下(json -> Python):
- 布尔类型(true与false)映射Python中布尔类型(True与False)。
- 空值类型(null)映射为None。
- 整数与浮点类型映射为整数(int)与(float)类型。
- 字符串类型映射为字符串(str)类型。
- 数组类型([])映射为列表(list)类型。
- 对象类型(object)映射为字典(dict)类型。
映射程序示例。
json在序列化时,不能序列化我们自定义的类型(以上类型之外的类型)。如果我们需要自定义的类型也能够序列化,可以定义一个编码类,该类继承json.JSONEncoder,实现类中的default方法,指定序列化的方式,同时,在调用序列化方法时(dump或dumps),使用cls参数指定我们定义的编码类。
自定义序列化程序
14.3.3pickle
我们也可以使用pickle模块提供的功能来序列化类型。在序列化自定义类型上,pickle可以比json模块更加方便(不需要定义类似的编码器类)。
pickle与json在序列化上的区别见下表。
表格 152 json与pickle在序列化上的区别
序列化 | json | pickle |
序列化格式 | 文本格式,可进行正常查看。 | 二进制格式,不方便查看。 |
序列化类型的支持 | 支持一部分内建的类型,如果需要序列化自定义类型,需要编写编码类。 | 支持非常广泛的类型,包括自定义类型,不需要编写编码类。 |
适用广泛性 | 适用广泛,对于序列化的内容可以用于Python语言之外的程序中进行反序列化。 | 适用受限,只能用于Python程序中,其他语言的程序无法反序列化。 |
Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据,不能成功地反序列化也没关系。
14.4上下文管理器
14.4.1自定义上下文管理器
我们之前介绍过with的使用。通过with,我们可以代替try-finally语句,实现更简洁的资源清理工作。然而,with为什么有这种神奇的特性呢?是什么给了with如此的保证?我们接下来就来介绍。
实际上,with语句跟随的表达式会返回一个上下文管理器,该上下文管理器中定义相关方法,在with开始执行与退出时会调用,也就是说,上下文管理器为with提供一个执行环境。
- __enter__(self)
with语句体开始执行时,会调用该方法。我们可以在该方法中执行某些初始化的操作,该方法的返回值会赋值给with语句中as后面的变量。
- __exit__(self, exc_type, exc_val, exc_tb)
with语句体执行结束后,会调用该方法。我们在__enter__方法中执行的初始化,就可以在该方法中执行相关的清理,例如,文件的关闭,线程锁的释放,状态的恢复等。这就可以实现于finally语句同样的功能。相关参数如下:
- exc_type:产生异常的类型。
- exc_val:产生异常类型的对象。
- exc_tb:traceback类型的对象,包含了异常产生位置的堆栈调用信息。
如果在with语句体中没有产生异常,则相关的参数(exc_type,exc_val与 exc_tb)的值为None。否则,异常类,异常对象与轨迹信息会依次传递给这三个参数,此时,如果该方法的返回值为True,则压制异常,即异常不会在with语句体结束后继续抛出,如果方法的返回值为False,则异常继续抛出。
对于with,也可以关联两个上下文管理器,例如:
with Manager1() as m1, Manager2 as m2:
语句
这相当于是:
with Manager1() as m1:
with Manager2() as m2:
语句
14.4.2@contextmanager装饰器
在contextlib模块中,定义了@contextmanager装饰器,该装饰器可以用来修饰一个生成器,从而将生成器变成一个上下文管理器,从而可以省略编写完整的上下文管理器类,在一定程度上可以简化程序。
关于@contextmanager装饰器,有以下几点说明:
- 在@contextmanager修饰的生成器中,yield之前的语句会在进入with语句体时执行(相当于__enter__方法),而yield之后的语句会在离开with语句体时执行(相当于__eixt__方法)。
- with后的表达式会返回生成器对象(假设为gen_obj),进入with语句体时,内部会调用next函数,用来激活生成器对象,进而执行生成器的函数体:
next(gen_obj)
- 从而令生成器对象执行。yield产生的值则会赋值给with语句as后的变量(相当于__enter__方法的返回值)。
- 当with语句体结束时,如果with语句体没有产生异常,则继续调用next,令生成器从之前yield暂停的位置处继续执行(这相当于实现__exit__方法)。如果with语句体产生异常,该异常会在生成器函数体yield的位置抛出。而如果生成器函数体没有处理该异常,将会导致yield之后的语句不会得到执行,这相当于是没有成功的执行__exit__方法。
因此,为了能够保证yield之后的语句能够得到执行,我们应该总是在生成器的函数体内使用try-finally语句。
第十五章 正则表达式
15.1从re模块开始
正则表达式可以对指定的字符串与模式之间执行模式匹配。模式可以是普通的字符串,也可以是含有特殊意义字符的字符串。通过正则表达式,我们可以进行查找,校验等用途。在Python中,我们可以使用re模块来实现正则表达式的模式匹配操作。现在,我们就从最简单的,模式为普通文本字符开始。
模式为普通文本字符,指正则表达式的模式中不含有特殊字符,完全是按照字符本身的内容进行匹配的,这种情况下,实际上就是判断带匹配的文本是否包含模式,与判断子串是否出现在某个主串的含义相同。
第一个正则表达式程序。
模式与待搜索的字符串除了可以是str类型,也可以是bytes类型,但是,二者类型必须一致,不能混淆,否则会产生错误。例如,以下两行代码都是非法的:
re.search(b"cd", "abcdefg")
re.search("cd", b"abcdefg")
疑问:正则表达式的匹配怎么就像是str的in操作呢?
15.2特殊字符
15.2.1字符相关
下表中的字符内容可以匹配单个字符。
表格 161 特殊字符
字符 | 说明 |
. | 默认模式下,匹配除换行符(\n)之外的所有单个字符。在S(DOTALL)模式下,匹配所有单个字符。 |
[字符] | 匹配[]内的任意一个字符。[]中可以是单个字符,如[x9k],也可以是一个字符区间,如[a-k],[3-5]。如果需要匹配“-”,可以使用“\-”转义,或者将该字符置于[]的两端,如[-axk]或[axk-]。如果需要匹配“]”,可以使用“\]”转义,或者将该字符置于[]的最前端,如[]axk]。 |
[^字符] | 匹配不在[]内的任意一个字符,[]的取反匹配。 |
\d | 如果是str类型,匹配Unicode十进制数字,这包括但不限于0 ~ 9,例如0,٧等字符,也能够匹配成功。如果是bytes类型,匹配[0-9]。 |
\D | 匹配非Unicode数字字符,\d的取反匹配。 |
\s | 如果是str类型,匹配Unicode空白符,这包括但不限于[空格\t\v\r\n\f]。如果是bytes类型,匹配[空格\t\v\r\n\f]。 |
\S | 匹配非Unicode空白字符,\s的取反匹配。 |
\w | 如果是str类型,匹配Unicode单词字符,这包括但不限于[a-zA-Z0-9_]。如果是bytes类型,则匹配[a-zA-Z0-9_]。 |
\W | 匹配非Unicode单词字符,\w的取反匹配。 |
\ | 转义字符,对正则表达式的特殊字符进行转义,例如,如果要匹配普通的“.”字符,则可以使用“\.”。 |
说明:
\ 在Python中是转义的开始,在正则表达式中也是转义的开始,因此,建议模式使用原始字符串,这样可以减少转义的繁琐性。
15.2.2次数相关
表格 162 特殊字符
字符 | 说明 |
* | 匹配前面的字符0次或多次。 |
+ | 匹配前面的字符1次或多次。 |
? | 匹配前面的字符0次或1次。 |
{m} | 匹配前面的字符m次。 |
{m,} | 匹配前面的字符至少m次。 |
{,n} | 匹配前面的字符至多n次。 |
{m,n} | 匹配前面的字符m到n次。 |
X? | X表示以上的任意一种模式({m}除外),即在对应的模式字符串后面加上一个问号?,表示该模式的非贪婪模式(否则为贪婪模式)。贪婪模式与非贪婪模式的区别在于:贪婪模式会尽可能匹配最多的字符,而非贪婪模式会尽可能匹配最少的字符。 |
15.2.3边界相关
字符 | 说明 |
^ | 匹配字符串的开头。在多行模式下,可以匹配每一行的开头。 |
$ | 匹配字符串的结尾。在多行模式下,可以匹配每一行的末尾。 |
\A | 仅匹配字符串的开头。 |
\Z | 仅匹配字符串的末尾。 |
\b | 匹配单词的边界。单词可以含有Unicode字符、数字与下划线组成(\w+匹配的内容)。\b匹配的是空串,该空串可以出现在\w(\W)与\W(\w)之间、字符串开头与\w之间或\w与字符串结尾之间。 |
\B | 匹配单词的非边界。\B匹配的是空串,该空串必须出现在两个\w之间。\B是\b的取反匹配。 |
15.2.4组相关
字符 | 说明 |
() | 对()内的字符进行分组。分组后,该组匹配的内容可以单独提取,同时,也可以在模式字符串后面使用\number进行引用。 |
\number | number用来指定组序号,序号从1开始。用来匹配number对应的分组内容。 |
(?:表达式) | 匹配()内的字符,但是不会进行分组。()内匹配的内容也无法单独提取,或者在后面使用\number引用。 |
(?P<name>表达式) | 对()内的字符进行分组,组名为name,多个组之间的名称不能重复。分组后,该组匹配的内容可以单独提取,同时,也可以在模式字符串后面使用(?P=name)或\number进行引用。对比之前()进行的序号分组,此种方式可以称为命名分组。不过,命名分组依然也可以使用序号(\number)进行引用。 |
(?P=name) | 用来匹配同名的分组内容【之前使用(?P<name>)进行的分组】。 |
| | 用来连接两个并列的模式字符串,匹配其中的一个即可。 |
15.2.5控制标记
正则表达式的第三个参数flag的使用。(3.6后,使用RegexFlag对象)
I(IGNORECASE)
M(MULTILINE)
S(DOTALL)
15.3相关属性与方法
15.3.1re模块的函数
- re.compile()
re.search()
- re.match()
- re.findall()
- re.finditer()
- re.split()
- re.sub()
15.3.2正则表达式对象
正则匹配成功后,会返回一个Match对象,该对象具有如下属性:
- .string 待匹配的文本
- .re 匹配时使用的patter对象(正则表达式)
- .pos 搜索文本的开始位置
- .endpos 搜索文本的结束位置
- .group() 获得匹配后的字符串
- groups()
- .start() 匹配字符串在原始字符串的开始位置
- .end() 匹配字符串在原始字符串的结束位置
- .span() 返回(.start(), .end())
此外,match对象也具有re函数提供的正则匹配的功能。