python(3)

目录

 

第十一章 模块和包

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.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.3手动抛出异常

12.4自定义异常

第十三章 迭代器、生成器、装饰器

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类装饰器

第十四章 文件

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函数

  • 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模块搜索路径

当我们导入模块时,解释器会按照如下顺序搜索:

  1. 在解释器内建模块中搜索指定的模块名。例如,sys,math等。
  2. 作为脚本运行文件所在的路径。
  3. PYTHONPATH环境变量指定的路径。我们可以根据需要设置该路径,如果没有该环境变量,则忽略。
  4. 与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. 常见异常类型

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

  1. ZeroDivisionError
  2. NameError
  3. TypeError
  4. AtrributeError
  5. Indentation
  6. IndexError
  7. UnboundLocalError
  8. AssertionError
  9. ModuleNotFoundError
  10. KeyError
  11. RecursionError
  12. StopIteration
  13. ValueError
  14. 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. 可迭代对象与迭代器

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实例的方式,来判断该对象是否为可迭代对象。

回忆:之前学习的类型,哪些可以用在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. 获取文件对象

14.1.2. 文件的读取

14.1.3. 文件的写入

14.1.4. 文件定位

14.2. 文件与路径的操作

14.2.1. os模块

14.2.2. os.path模块

14.2.3. shutil模块

14.3. 序列化

14.3.1. csv

14.3.2. json

14.3.3. pickle

14.4. 上下文管理器

14.4.1. 自定义上下文管理器

14.4.2. @contextmanager装饰器

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后面的变量。

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模块开始

15.2. 特殊字符

15.2.1. 字符相关

15.2.2. 次数相关

15.2.3. 边界相关

15.2.4. 组相关

15.2.5. 控制标记

15.3. 相关属性与方法

15.3.1. re模块的函数

15.3.2. 正则表达式对象

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函数提供的正则匹配的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值