文章目录
系列目录
Python|Git remote|hosts|PyCharm常用快捷键|变量转换|命名|类型|运算符|分支|调整tab|循环|语言基础50课:学习记录(1)-项目简介及变量、条件及循环
Python|list|切片|列表的运算符、比较及遍历|生成式|元素位置和次数|元素排序和反转|sort() 方法|嵌套的列表|语言基础50课:学习记录(2)-常用数据结构之列表
Python|元组|字符串|语言基础50课:学习记录(3)-常用数据结构之元组及字符串相关
Python|集合|运算|哈希码|语言基础50课:学习记录(4)-常用数据结构之集合
Python|字典|函数和模块|应用及进阶|分数符号(Latex)|String库|operator库|处理数据三步骤|语言基础50课:学习记录(5)-常用数据结构之字典、函数和模块应用及进阶
Python|装饰器|执行时间|递归|动态属性|静态方法和类|继承和多态|isinstance类型判断|溢出|“魔法”方法|语言基础50课:学习记录(6)-函数的高级应用、面向对象编程、进阶及应用
Python|base64|collections|hashlib|heapq|itertools|random|os.path|uuid|文件|异常|JSON|API|CSV|语言基础50课:学习7
Python|xlwt|xlrd|调整单元格样式(背景,字体,对齐、虚线边框、列宽行高、添加公式)|xlutils|openpyxl|只读与只写|图表|语言基础50课:学习(8)
Python|python-docx|python-pptx|Pillow|smtplib|螺丝帽短信网关|正则表达式的应用|语言基础50课:学习(9)
Python|http|Chrome Developer Tools|Postman|HTTPie|builtwith库|python-whois库|爬虫及解析|语言基础50课:学习(10)
Python|线程和进程|阻塞|非阻塞|同步|异步|生成器和协程|资源竞争|进程间通信|aiohttp库|daemon属性值详解|语言基础50课:学习(11)
Python|并发编程|爬虫|单线程|多线程|异步I/O|360图片|Selenium及JavaScript|Scrapy框架|BOM 和 DOM 操作简介|语言基础50课:学习(12)
Python|MySQL概述|Windows-Linux-macOS安装|MySQL 基本命令|获取帮助|SQL注释|语言基础50课:学习(13)
Python|SQL详解之DDL|DML|DQL|DCL|索引|视图、函数和过程|JSON类型|窗口函数|接入MySQL|清屏|正则表达式|executemany|语言基础50课:学习(14)
原项目地址:
Python-Core-50-Courses(https://hub.fastgit.org/jackfrued/Python-Core-50-Courses.git)
第20课:Python标准库初探(常用模块)
base64 - Base64编解码模块
Base64是一种基于64个可打印字符来表示二进制数据的方法。由于
l
o
g
2
64
=
6
log _{2}64=6
log264=6,所以Base64以6个比特(二进制位,可以表示0或1)为一个单元,每个单元对应一个可打印字符。对于3字节(24比特)的二进制数据,我们可以将其处理成对应于4个Base64单元,即3个字节可由4个可打印字符来表示。Base64编码可用来作为电子邮件的传输编码,也可以用于其他需要将二进制数据转成文本字符的场景,这使得在XML、JSON、YAML这些文本数据格式中传输二进制内容成为可能。在Base64中的可打印字符包括A-Z
、a-z
、0-9
,这里一共是62个字符,另外两个可打印符号通常是+
和/
,=
用于在Base64编码最后进行补位。
关于Base64编码的细节,大家可以参考《Base64笔记》一文,Python标准库中的base64
模块提供了b64encode
和b64decode
两个函数,专门用于实现Base64的编码和解码,下面演示了在Python的交互式环境中执行这两个函数的效果。
>>> import base64
>>>
>>> content = 'Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.'
>>> base64.b64encode(content.encode()) #content.encode()先转换为bytes 类型字符串,b64encode再转换成bytes 类型base64字符串
#b" "前缀表示:后面字符串是bytes 类型。网络编程中,服务器和浏览器只认bytes 类型数据。
b'TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4='
>>> content = b'TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4='
>>> base64.b64decode(content).decode() #b64decode()后是bytes 类型,再decode()变成utf-8(本地字符串)
'Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.'
collections - 容器数据类型模块
collections
模块提供了诸多非常好用的数据结构,主要包括:
namedtuple
:命令元组,它是一个类工厂,接受类型的名称和属性列表来创建一个类。deque
:双端队列,是列表的替代实现。Python中的列表底层是基于数组来实现的,而deque
底层是双向链表,因此当你需要在头尾添加和删除元素是,deque
会表现出更好的性能,渐近时间复杂度为 O ( 1 ) O(1) O(1)。Counter
:dict
的子类,键是元素,值是元素的计数,它的most_common()
方法可以帮助我们获取出现频率最高的元素。Counter
和dict
的继承关系我认为是值得商榷的,按照CARP原则,Counter
跟dict
的关系应该设计为关联关系更为合理。- 合成复用原则(Composition/Aggregation Reuse Principle,CARP)强调在设计软件时,应该优先使用对象组合、聚合等方式来实现代码复用,而不是继承。这样可以减少类之间的耦合度,提高代码的可维护性和可重用性。
OrderedDict
:dict
的子类,它记录了键值对插入的顺序,看起来既有字典的行为,也有链表的行为。defaultdict
:类似于字典类型,但是可以通过默认的工厂函数来获得键对应的默认值,相比字典中的setdefault()
方法,这种做法更加高效。
下面是在Python交互式环境中使用namedtuple
创建扑克牌类的例子。
>>> from collections import namedtuple
>>>
>>> Card = namedtuple('Card', ('suite', 'face'))
>>> card1 = Card('红桃', 5)
>>> card2 = Card('草花', 9)
>>> card1
Card(suite='红桃', face=5)
>>> card2
Card(suite='草花', face=9)
>>> print(f'{card1.suite}{card1.face}')
红桃5
>>> print(f'{card2.suite}{card2.face}')
草花9
下面是使用Counter
类统计列表中出现次数最多的三个元素的例子。
from collections import Counter
words = [
'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around',
'the', 'eyes', "don't", 'look', 'around', 'the', 'eyes',
'look', 'into', 'my', 'eyes', "you're", 'under'
]
counter = Counter(words)
# 打印words列表中出现频率最高的3个元素及其出现次数
for elem, count in counter.most_common(3):
print(elem, count)
hashlib - 哈希函数模块
哈希函数又称哈希算法或散列函数,是一种为已有的数据创建“数字指纹”(哈希摘要)的方法。哈希函数把数据压缩成摘要,对于相同的输入,哈希函数可以生成相同的摘要(数字指纹),需要注意的是这个过程并不可逆(不能通过摘要计算出输入的内容)。一个优质的哈希函数能够为不同的输入生成不同的摘要,出现哈希冲突(不同的输入产生相同的摘要)的概率极低,MD5、SHA家族就是这类好的哈希函数。
说明:在2011年的时候,RFC 6151中已经禁止将MD5用作密钥散列消息认证码,这个问题不在我们讨论的范围内。
Python标准库的hashlib
模块提供了对哈希函数的封装,通过使用md5
、sha1
、sha256
等类,我们可以轻松的生成“数字指纹”。举一个简单的例子,用户注册时我们希望在数据库中保存用户的密码,很显然我们不能将用户密码直接保存在数据库中,这样可能会导致用户隐私的泄露,所以在数据库中保存用户密码时,通常都会将密码的“指纹”保存起来,用户登录时通过哈希函数计算密码的“指纹”再进行匹配来判断用户登录是否成功。
import hashlib
# 计算字符串"123456"的MD5摘要
#help(hashlib.md5)
#hash.hexdigest():返回摘要,作为十六进制数据字符串值
print(hashlib.md5('123456'.encode()).hexdigest()) #'123456'.encode()转化为byte类型,
#e10adc3949ba59abbe56e057f20f883e
#hash.digest() :返回摘要,作为二进制数据字符串值
print(hashlib.md5('123456'.encode()).digest())
#b'\xe1\n\xdc9I\xbaY\xab\xbeV\xe0W\xf2\x0f\x88>'
# 计算文件"Python-3.7.1.tar.xz"的MD5摘要
hasher = hashlib.md5()
with open('Python-3.7.1.tar.xz', 'rb') as file:
data = file.read(512)
#以512字节为单位持续读取,直到文件末尾,即对整个文件制作摘要
#增加空格,不会对文件产生影响,修改任意字符,哈希值变化。
while data:
hasher.update(data)
#用提供的字节串更新此哈希对象(hash object)的状态。
data = file.read(512)
print(hasher.hexdigest())
说明:很多网站在下载链接的旁边都提供了哈希摘要,完成文件下载后,我们可以计算该文件的哈希摘要并检查它与网站上提供的哈希摘要是否一致(指纹比对)。如果计算出的哈希摘要与网站提供的并不一致,很有可能是下载出错或该文件在传输过程中已经被篡改,这时候就不应该直接使用这个文件。
heapq - 堆排序模块
heapq
模块实现了堆排序算法,如果希望使用堆排序,尤其是要解决TopK问题(从序列中找到K个最大或最小元素),直接使用该模块即可,代码如下所示。
import heapq
list1 = [34, 25, 12, 99, 87, 63, 58, 78, 88, 92]
# 找出列表中最大的三个元素
print(heapq.nlargest(3, list1))
# 找出列表中最小的三个元素
print(heapq.nsmallest(3, list1))
list2 = [
{'name': 'IBM', 'shares': 100, 'price': 91.1},
{'name': 'AAPL', 'shares': 50, 'price': 543.22},
{'name': 'FB', 'shares': 200, 'price': 21.09},
{'name': 'HPQ', 'shares': 35, 'price': 31.75},
{'name': 'YHOO', 'shares': 45, 'price': 16.35},
{'name': 'ACME', 'shares': 75, 'price': 115.65}
]
# 找出价格最高的三只股票 引用字典的某一个字段作为可以,需要使用lambda函数
print(heapq.nlargest(3, list2, key=lambda x: x['price']))
# 找出持有数量最高的三只股票
print(heapq.nlargest(3, list2, key=lambda x: x['shares']))
itertools - 迭代工具模块
itertools
可以帮助我们生成各种各样的迭代器,大家可以看看下面的例子。
import itertools
# 产生ABCD的全排列
for value in itertools.permutations('ABCD'):
print(value)
# 产生ABCDE的五选三组合
for value in itertools.combinations('ABCDE', 3):
print(value)
# 产生ABCD和123的笛卡尔积
for value in itertools.product('ABCD', '123'):
print(value)
# 产生ABC的无限循环序列
it = itertools.cycle(('A', 'B', 'C'))
print(next(it))
print(next(it))
print(next(it))
print(next(it))
random - 随机数和随机抽样模块
这个模块我们之前已经用过很多次了,生成随机数、实现随机乱序和随机抽样,下面是常用函数的列表。
getrandbits(k)
:返回具有k
个随机比特位的整数。randrange(start, stop[, step])
:从range(start, stop, step)
返回一个随机选择的元素,但实际上并没有构建一个range
对象。randint(a, b)
:返回随机整数N
满足a <= N <= b
,相当于randrange(a, b+1)
。choice(seq)
:从非空序列seq
返回一个随机元素。 如果seq
为空,则引发IndexError
。choices(population, weight=None, *, cum_weights=None, k=1)
:从population
中选择替换,返回大小为k
的元素列表。 如果population
为空,则引发IndexError
。shuffle(x[, random])
:将序列x
随机打乱位置。sample(population, k)
:返回从总体序列或集合中选择k
个不重复元素构造的列表,用于无重复的随机抽样。random()
:返回[0.0, 1.0)
范围内的下一个随机浮点数。expovariate(lambd)
:指数分布。gammavariate(alpha, beta)
:伽玛分布。gauss(mu, sigma)
/normalvariate(mu, sigma)
:正态分布。paretovariate(alpha)
:帕累托分布。weibullvariate(alpha, beta)
:威布尔分布。
os.path - 路径操作相关模块
os.path
模块封装了操作路径的工具函数,如果程序中需要对文件路径做拼接、拆分、获取以及获取文件的存在性和其他属性,这个模块将会非常有帮助,下面为大家罗列一些常用的函数。
dirname(path)
:返回路径path
的目录名称。exists(path)
:如果path
指向一个已存在的路径或已打开的文件描述符,返回True
。getatime(path)
/getmtime(path)
/getctime(path)
:返回path
的最后访问时间/最后修改时间/创建时间。getsize(path)
:返回path
的大小,以字节为单位。如果该文件不存在或不可访问,则抛出OSError
异常。isfile(path)
:如果path
是普通文件,则返回True
。isdir(path)
:如果path
是目录(文件夹),则返回True
。join(path, *paths)
:合理地拼接一个或多个路径部分。返回值是path
和paths
所有值的连接,每个非空部分后面都紧跟一个目录分隔符 (os.sep
),除了最后一部分。这意味着如果最后一部分为空,则结果将以分隔符结尾。如果参数中某个部分是绝对路径,则绝对路径前的路径都将被丢弃,并从绝对路径部分开始连接。splitext(path)
:将路径path
拆分为一对,即(root, ext)
,使得root + ext == path
,其中ext
为空或以英文句点开头,且最多包含一个句点。
uuid - UUID生成模块
uuid
模块可以帮助我们生成全局唯一标识符(Universal Unique IDentity)。该模块提供了四个用于生成UUID的函数,分别是:
uuid1()
:由MAC地址、当前时间戳、随机数生成,可以保证全球范围内的唯一性。uuid3(namespace, name)
:通过计算命名空间和名字的MD5哈希摘要(“指纹”)值得到,保证了同一命名空间中不同名字的唯一性,和不同命名空间的唯一性,但同一命名空间的同一名字会生成相同的UUID。uuid4()
:由伪随机数生成UUID,有一定的重复概率,该概率可以计算出来。uuid5()
:算法与uuid3
相同,只不过哈希函数用SHA-1取代了MD5。
由于uuid4
存在概率型重复,那么在真正需要全局唯一标识符的地方最好不用使用它。在分布式环境下,uuid1
是很好的选择,因为它能够保证生成ID的全局唯一性。下面是在Python交互式环境中使用uuid1
函数生成全局唯一标识符的例子。
>>> import uuid
>>> uuid.uuid1().hex
'622a8334baab11eaaa9c60f81da8d840'
>>> uuid.uuid1().hex
'62b066debaab11eaaa9c60f81da8d840'
>>> uuid.uuid1().hex
'642c0db0baab11eaaa9c60f81da8d840'
第21课:文件读写和异常处理
实际开发中常常会遇到对数据进行持久化的场景,所谓持久化是指将数据从无法长久保存数据的存储介质(通常是内存)转移到可以长久保存数据的存储介质(通常是硬盘)中。实现数据持久化最直接简单的方式就是通过文件系统将数据保存到文件中。
计算机的文件系统是一种存储和组织计算机数据的方法,它使得对数据的访问和查找变得容易,文件系统使用文件和树形目录的抽象逻辑概念代替了硬盘、光盘、闪存等物理设备的数据块概念,用户使用文件系统来保存数据时,不必关心数据实际保存在硬盘的哪个数据块上,只需要记住这个文件的路径和文件名。在写入新数据之前,用户不必关心硬盘上的哪个数据块没有被使用,硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成,用户只需要记住数据被写入到了哪个文件中。
打开和关闭文件
有了文件系统,我们可以非常方便的通过文件来读写数据;在Python中要实现文件操作是非常简单的。我们可以使用Python内置的open
函数来打开文件,在使用open
函数时,我们可以通过函数的参数指定文件名、操作模式和字符编码等信息,接下来就可以对文件进行读写操作了。这里所说的操作模式是指要打开什么样的文件(字符文件或二进制文件)以及做什么样的操作(读、写或追加),具体如下表所示。
操作模式 | 具体含义 |
---|---|
'r' | 读取 (默认) |
'w' | 写入(会先截断之前的内容) |
'x' | 写入,如果文件已经存在会产生异常 |
'a' | 追加,将内容写入到已有文件的末尾 |
'b' | 二进制模式 |
't' | 文本模式(默认) |
'+' | 更新(既可以读又可以写) |
下图展示了如何根据程序的需要来设置open
函数的操作模式。
在使用open
函数时,如果打开的文件是字符文件(文本文件),可以通过encoding
参数来指定读写文件使用的字符编码。如果对字符编码和字符集这些概念不了解,可以看看《字符集和字符编码》一文,此处不再进行赘述。
使用open
函数打开文件成功后会返回一个文件对象,通过这个对象,我们就可以实现对文件的读写操作;如果打开文件失败,open
函数会引发异常,稍后会对此加以说明。如果要关闭打开的文件,可以使用文件对象的close
方法,这样可以在结束文件操作时释放掉这个文件。
读写文本文件
用open
函数打开文本文件时,需要指定文件名并将文件的操作模式设置为'r'
,如果不指定,默认值也是'r'
;如果需要指定字符编码,可以传入encoding
参数,如果不指定,默认值是None,那么在读取文件时使用的是操作系统默认的编码。需要提醒大家,如果不能保证保存文件时使用的编码方式与encoding
参数指定的编码方式是一致的,那么就可能因无法解码字符而导致读取文件失败。
下面的例子演示了如何读取一个纯文本文件(一般指只有字符原生编码构成的文件,与富文本相比,纯文本不包含字符样式的控制元素,能够被最简单的文本编辑器直接读取)。
file = open('致橡树.txt', 'r', encoding='utf-8')
print(file.read())
file.close()
除了使用文件对象的read
方法读取文件之外,还可以使用for-in
循环逐行读取或者用readlines
方法将文件按行读取到一个列表容器中,代码如下所示。
file = open('致橡树.txt', 'r', encoding='utf-8')
for line in file:
print(line, end='')
file.close()
file = open('致橡树.txt', 'r', encoding='utf-8')
lines = file.readlines()
for line in lines:
print(line, end='')
file.close()
如果要向文件中写入内容,可以在打开文件时使用w
或者a
作为操作模式,前者会截断之前的文本内容写入新的内容,后者是在原来内容的尾部追加新的内容。
file = open('致橡树.txt', 'a', encoding='utf-8')
file.write('\n标题:《致橡树》')
file.write('\n作者:舒婷')
file.write('\n时间:1977年3月')
file.close()
异常处理机制
请注意上面的代码,如果open
函数指定的文件并不存在或者无法打开,那么将引发异常状况导致程序崩溃。为了让代码具有健壮性和容错性,我们可以使用Python的异常机制对可能在运行时发生状况的代码进行适当的处理。Python中和异常相关的关键字有五个,分别是try
、except
、else
、finally
和raise
,我们先看看下面的代码,再来为大家介绍这些关键字的用法。
file = None
try:
file = open('致橡树.txt', 'r', encoding='utf-8')
print(file.read())
except FileNotFoundError:
print('无法打开指定的文件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取文件时解码错误!')
finally:
if file:
file.close()
在Python中,我们可以将运行时会出现状况的代码放在try
代码块中,在try
后面可以跟上一个或多个except
块来捕获异常并进行相应的处理。例如,在上面的代码中,文件找不到会引发FileNotFoundError
,指定了未知的编码会引发LookupError
,而如果读取文件时无法按指定编码方式解码文件会引发UnicodeDecodeError
,所以我们在try
后面跟上了三个except
分别处理这三种不同的异常状况。在except
后面,我们还可以加上else
代码块,这是try
中的代码没有出现异常时会执行的代码,而且else
中的代码不会再进行异常捕获,也就是说如果遇到异常状况,程序会因异常而终止并报告异常信息。最后我们使用finally
代码块来关闭打开的文件,释放掉程序中获取的外部资源。由于finally
块的代码不论程序正常还是异常都会执行,甚至是调用了sys
模块的exit
函数终止Python程序,finally
块中的代码仍然会被执行(因为exit
函数的本质是引发了SystemExit
异常),因此我们把finally
代码块称为“总是执行代码块”,它最适合用来做释放外部资源的操作。
Python中内置了大量的异常类型,除了上面代码中用到的异常类型以及之前的课程中遇到过的异常类型外,还有许多的异常类型,其继承结构如下所示。
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
从上面的继承结构可以看出,Python中所有的异常都是BaseException
的子类型,它有四个直接的子类,分别是:SystemExit
、KeyboardInterrupt
、GeneratorExit
和Exception
。其中,SystemExit
表示解释器请求退出,KeyboardInterrupt
是用户中断程序执行(按下Ctrl+c
),GeneratorExit
表示生成器发生异常通知退出,不理解这些异常没有关系,继续学习就好了。值得一提的是Exception
类,它是常规异常类型的父类型,很多的异常都是直接或间接的继承自Exception
类。如果Python内置的异常类型不能满足应用程序的需要,我们可以自定义异常类型,而自定义的异常类型也应该直接或间接继承自Exception
类,当然还可以根据需要重写或添加方法。
在Python中,可以使用raise
关键字来引发异常(抛出异常对象),而调用者可以通过try...except...
结构来捕获并处理异常。例如在函数中,当函数的执行条件不满足时,可以使用抛出异常的方式来告知调用者问题的所在,而调用者可以通过捕获处理异常来使得代码从异常中恢复,定义异常和抛出异常的代码如下所示。
class InputError(ValueError):
"""自定义异常类型"""
pass
def fac(num):
"""求阶乘"""
if num < 0:
raise InputError('只能计算非负整数的阶乘')
if num in (0, 1):
return 1
return num * fac(num - 1)
调用求阶乘的函数fac
,通过try...except...
结构捕获输入错误的异常并打印异常对象(显示异常信息),如果输入正确就计算阶乘并结束程序。
flag = True
while flag:
num = int(input('n = '))
try:
print(f'{num}! = {fac(num)}')
flag = False
except InputError as err:
print(err)
上下文语法
对于open
函数返回的文件对象,还可以使用with
上下文语法在文件操作完成后自动执行文件对象的close
方法,这样可以让代码变得更加简单优雅,因为不需要再写finally
代码块来执行关闭文件释放资源的操作。需要提醒大家的是,并不是所有的对象都可以放在with
上下文语法中,只有符合上下文管理器协议(有__enter__
和__exit__
魔术方法)的对象才能使用这种语法,Python标准库中的contextlib
模块也提供了对with
上下文语法的支持,后面再为大家进行讲解。
用with
上下文语法改写后的代码如下所示。
try:
with open('致橡树.txt', 'r', encoding='utf-8') as file:
print(file.read())
except FileNotFoundError:
print('无法打开指定的文件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取文件时解码错误!')
读写二进制文件
读写二进制文件跟读写文本文件的操作类似,但是需要注意,在使用open
函数打开文件时,如果要进行读操作,操作模式是'rb'
,如果要进行写操作,操作模式是'wb'
。还有一点,读写文本文件时,read
方法的返回值以及write
方法的参数是str
对象(字符串),而读写二进制文件时,read
方法的返回值以及write
方法的参数是bytes-like
对象(字节串)。下面的代码实现了将当前路径下名为guido.jpg
的图片文件复制到吉多.jpg
文件中的操作。
try:
with open('guido.jpg', 'rb') as file1:
data = file1.read()
with open('吉多.jpg', 'wb') as file2:
file2.write(data)
except FileNotFoundError:
print('指定的文件无法打开.')
except IOError:
print('读写文件时出现错误.')
print('程序执行结束.')
如果要复制的图片文件很大,一次将文件内容直接读入内存中可能会造成非常大的内存开销,为了减少对内存的占用,可以为read
方法传入size
参数来指定每次读取的字节数,通过循环读取和写入的方式来完成上面的操作,代码如下所示。
try:
with open('guido.jpg', 'rb') as file1, open('吉多.jpg', 'wb') as file2:
data = file1.read(512)
while data:
file2.write(data)
data = file1.read()
except FileNotFoundError:
print('指定的文件无法打开.')
except IOError:
print('读写文件时出现错误.')
print('程序执行结束.')
简单的总结
通过读写文件的操作,我们可以实现数据持久化。在Python中可以通过open
函数来获得文件对象,可以通过文件对象的read
和write
方法实现文件读写操作。程序在运行时可能遭遇无法预料的异常状况,可以使用Python的异常机制来处理这些状况。Python的异常机制主要包括try
、except
、else
、finally
和raise
这五个核心关键字。try
后面的except
语句不是必须的,finally
语句也不是必须的,但是二者必须要有一个;except
语句可以有一个或多个,多个except
会按照书写的顺序依次匹配指定的异常,如果异常已经处理就不会再进入后续的except
语句;except
语句中还可以通过元组同时指定多个异常类型进行捕获;except
语句后面如果不指定异常类型,则默认捕获所有异常;捕获异常后可以使用raise
要再次抛出,但是不建议捕获并抛出同一个异常;不建议在不清楚逻辑的情况下捕获所有异常,这可能会掩盖程序中严重的问题。最后强调一点,不要使用异常机制来处理正常业务逻辑或控制程序流程,简单的说就是不要滥用异常机制,这是初学者常犯的错误。
第22课:对象的序列化和反序列化
JSON概述
通过上面的讲解,我们已经知道如何将文本数据和二进制数据保存到文件中,那么这里还有一个问题,如果希望把一个列表或者一个字典中的数据保存到文件中又该怎么做呢?在Python中,我们可以将程序中的数据以JSON格式进行保存。JSON是“JavaScript Object Notation”的缩写,它本来是JavaScript语言中创建对象的一种字面量语法,现在已经被广泛的应用于跨语言跨平台的数据交换。使用JSON的原因非常简单,因为它结构紧凑而且是纯文本,任何操作系统和编程语言都能处理纯文本,这就是实现跨语言跨平台数据交换的前提条件。目前JSON基本上已经取代了XML(可扩展标记语言)作为异构系统间交换数据的事实标准。可以在JSON的官方网站找到更多关于JSON的知识,这个网站还提供了每种语言处理JSON数据格式可以使用的工具或三方库。
{
name: "骆昊",
age: 40,
friends: ["王大锤", "白元芳"],
cars: [
{"brand": "BMW", "max_speed": 240},
{"brand": "Benz", "max_speed": 280},
{"brand": "Audi", "max_speed": 280}
]
}
上面是JSON的一个简单例子,大家可能已经注意到了,它跟Python中的字典非常类似而且支持嵌套结构,就好比Python字典中的值可以是另一个字典。我们可以尝试把下面的代码输入浏览器的控制台(对于Chrome浏览器,可以通过“更多工具”菜单找到“开发者工具”子菜单,就可以打开浏览器的控制台),浏览器的控制台提供了一个运行JavaScript代码的交互式环境(类似于Python的交互式环境),下面的代码会帮我们创建出一个JavaScript的对象,我们将其赋值给名为obj
的变量。
let obj = {
name: "骆昊",
age: 40,
friends: ["王大锤", "白元芳"],
cars: [
{"brand": "BMW", "max_speed": 240},
{"brand": "Benz", "max_speed": 280},
{"brand": "Audi", "max_speed": 280}
]
}
上面的obj
就是JavaScript中的一个对象,我们可以通过obj.name
或obj["name"]
两种方式获取到name
对应的值,如下图所示。可以注意到,obj["name"]
这种获取数据的方式跟Python字典通过键获取值的索引操作是完全一致的,而Python中也通过名为json
的模块提供了字典与JSON双向转换的支持。
我们在JSON中使用的数据类型(JavaScript数据类型)和Python中的数据类型也是很容易找到对应关系的,大家可以看看下面的两张表。
表1:JavaScript数据类型(值)对应的Python数据类型(值)
JSON | Python |
---|---|
object | dict |
array | list |
string | str |
number | int / float |
number (real) | float |
boolean (true / false ) | bool (True / False ) |
null | None |
表2:Python数据类型(值)对应的JavaScript数据类型(值)
Python | JSON |
---|---|
dict | object |
list / tuple | array |
str | string |
int / float | number |
bool (True / False ) | boolean (true / false ) |
None | null |
读写JSON格式的数据
在Python中,如果要将字典处理成JSON格式(以字符串形式存在),可以使用json
模块的dumps
函数,代码如下所示。
import json
my_dict = {
'name': '骆昊',
'age': 40,
'friends': ['王大锤', '白元芳'],
'cars': [
{'brand': 'BMW', 'max_speed': 240},
{'brand': 'Audi', 'max_speed': 280},
{'brand': 'Benz', 'max_speed': 280}
]
}
print(json.dumps(my_dict))
运行上面的代码,输出如下所示,可以注意到中文字符都是用Unicode编码显示的。
{"name": "\u9a86\u660a", "age": 40, "friends": ["\u738b\u5927\u9524", "\u767d\u5143\u82b3"], "cars": [{"brand": "BMW", "max_speed": 240}, {"brand": "Audi", "max_speed": 280}, {"brand": "Benz", "max_speed": 280}]}
如果要将字典处理成JSON格式并写入文本文件,只需要将dumps
函数换成dump
函数并传入文件对象即可,代码如下所示。
import json
my_dict = {
'name': '骆昊',
'age': 40,
'friends': ['王大锤', '白元芳'],
'cars': [
{'brand': 'BMW', 'max_speed': 240},
{'brand': 'Audi', 'max_speed': 280},
{'brand': 'Benz', 'max_speed': 280}
]
}
with open('data.json', 'w') as file:
json.dump(my_dict, file)
执行上面的代码,会创建data.json
文件,文件的内容跟上面代码的输出是一样的。
json
模块有四个比较重要的函数,分别是:
dump
- 将Python对象按照JSON格式序列化到文件中dumps
- 将Python对象处理成JSON格式的字符串load
- 将文件中的JSON数据反序列化成对象loads
- 将字符串的内容反序列化成Python对象
这里出现了两个概念,一个叫序列化,一个叫反序列化,维基百科上的解释是:“序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换为可以存储或传输的形式,这样在需要的时候能够恢复到原先的状态,而且通过序列化的数据重新获取字节时,可以利用这些字节来产生原始对象的副本(拷贝)。与这个过程相反的动作,即从一系列字节中提取数据结构的操作,就是反序列化(deserialization)”。
我们可以通过下面的代码,读取上面创建的data.json
文件,将JSON格式的数据还原成Python中的字典。
import json
with open('data.json', 'r') as file:
my_dict = json.load(file)
print(type(my_dict))
print(my_dict)
包管理工具pip的使用
Python标准库中的json
模块在数据序列化和反序列化时性能并不是非常理想,为了解决这个问题,可以使用三方库ujson
来替换json
。所谓三方库,是指非公司内部开发和使用的,也不是来自于官方标准库的Python模块,这些模块通常由其他公司、组织或个人开发,所以被称为三方库。虽然Python语言的标准库虽然已经提供了诸多模块来方便我们的开发,但是对于一个强大的语言来说,它的生态圈一定也是非常繁荣的。
之前安装Python解释器时,默认情况下已经勾选了安装pip,大家可以在命令提示符或终端中通过pip --version
来确定是否已经拥有了pip。pip是Python的包管理工具,通过pip可以查找、安装、卸载、更新Python的三方库或工具,macOS和Linux系统应该使用pip3。例如要安装替代json
模块的ujson
,可以使用下面的命令。
pip install ujson
在默认情况下,pip会访问https://pypi.org/simple/
来获得三方库相关的数据,但是国内访问这个网站的速度并不是十分理想,因此国内用户可以使用豆瓣网提供的镜像来替代这个默认的下载源,操作如下所示。
pip install ujson
可以通过pip search
命令根据名字查找需要的三方库,可以通过pip list
命令来查看已经安装过的三方库。如果想更新某个三方库,可以使用pip install -U
或pip install --upgrade
;如果要删除某个三方库,可以使用pip uninstall
命令。
搜索ujson
三方库。
pip search ujson
micropython-cpython-ujson (0.2) - MicroPython module ujson ported to CPython
pycopy-cpython-ujson (0.2) - Pycopy module ujson ported to CPython
ujson (3.0.0) - Ultra fast JSON encoder and decoder for Python
ujson-bedframe (1.33.0) - Ultra fast JSON encoder and decoder for Python
ujson-segfault (2.1.57) - Ultra fast JSON encoder and decoder for Python. Continuing
development.
ujson-ia (2.1.1) - Ultra fast JSON encoder and decoder for Python (Internet
Archive fork)
ujson-x (1.37) - Ultra fast JSON encoder and decoder for Python
ujson-x-legacy (1.35.1) - Ultra fast JSON encoder and decoder for Python
drf_ujson (1.2) - Django Rest Framework UJSON Renderer
drf-ujson2 (1.6.1) - Django Rest Framework UJSON Renderer
ujsonDB (0.1.0) - A lightweight and simple database using ujson.
fast-json (0.3.2) - Combines best parts of json and ujson for fast serialization
decimal-monkeypatch (0.4.3) - Python 2 performance patches: decimal to cdecimal, json to
ujson for psycopg2
查看已经安装的三方库。
pip list
Package Version
----------------------------- ----------
aiohttp 3.5.4
alipay 0.7.4
altgraph 0.16.1
amqp 2.4.2
... ...
更新ujson
三方库。
pip install -U ujson
删除ujson
三方库。
pip uninstall -y ujson
提示:如果要更新
pip
自身,对于macOS系统来说,可以使用命令pip install -U pip
。在Windows系统上,可以将命令替换为python -m pip install -U --user pip
。
使用网络API获取数据
如果想在我们自己的程序中显示天气、路况、航班等信息,这些信息我们自己没有能力提供,所以必须使用网络数据服务。目前绝大多数的网络数据服务(或称之为网络API)都是基于HTTP或HTTPS提供JSON格式的数据,我们可以通过Python程序发送HTTP请求给指定的URL(统一资源定位符),这个URL就是所谓的网络API,如果请求成功,它会返回HTTP响应,而HTTP响应的消息体中就有我们需要的JSON格式的数据。关于HTTP的相关知识,可以看看阮一峰的《HTTP协议入门》一文。
国内有很多提供网络API接口的网站,例如聚合数据、阿凡达数据等,这些网站上有免费的和付费的数据接口,国外的{API}Search网站也提供了类似的功能,有兴趣的可以自行研究。下面的例子演示了如何使用requests
库(基于HTTP进行网络资源访问的三方库)访问网络API获取国内新闻并显示新闻标题和链接。在这个例子中,我们使用了名为天行数据的网站提供的国内新闻数据接口,其中的APIKey需要自己到网站上注册申请。在天行数据网站注册账号后会自动分配APIKey,但是要访问接口获取数据,需要绑定验证邮箱或手机,然后还要申请需要使用的接口,如下图所示。
Python通过URL接入网络,我们推荐大家使用requests
三方库,它简单且强大,但需要自行安装。
pip install requests
获取国内新闻并显示新闻标题和链接。
import requests
APIKey='ad342049636b20f**************'
resp = requests.get('http://api.tianapi.com/guonei/?key='+APIKey+'&num=10')
if resp.status_code == 200:
data_model = resp.json()
for news in data_model['newslist']:
print(news['title'])
print(news['url'])
print('-' * 60)
上面的代码通过requests
模块的get
函数向天行数据的国内新闻接口发起了一次请求,如果请求过程没有出现问题,get
函数会返回一个Response
对象,通过该对象的status_code
属性表示HTTP响应状态码,如果不理解没关系,你只需要关注它的值,如果值等于200
或者其他2
字头的值,那么我们的请求是成功的。通过Response
对象的json()
方法可以将返回的JSON格式的数据直接处理成Python字典,非常方便。天行数据国内新闻接口返回的JSON格式的数据(部分)如下图所示。
提示:上面代码中的APIKey需要换成自己在天行数据网站申请的APIKey。天行数据网站上还有提供了很多非常有意思的API接口,例如:垃圾分类、周公解梦等,大家可以仿照上面的代码来调用这些接口。每个接口都有对应的接口文档,文档中有关于如何使用接口的详细说明。
简单的总结
Python中实现序列化和反序列化除了使用json
模块之外,还可以使用pickle
和shelve
模块,但是这两个模块是使用特有的序列化协议来序列化数据,因此序列化后的数据只能被Python识别,关于这两个模块的相关知识,有兴趣的读者可以自己查找网络上的资料。处理JSON格式的数据很显然是程序员必须掌握的一项技能,因为不管是访问网络API接口还是提供网络API接口给他人使用,都需要具备处理JSON格式数据的相关知识。
第23课:用Python读写CSV文件
CSV文件介绍
CSV(Comma Separated Values)全称逗号分隔值文件是一种简单、通用的文件格式,被广泛的应用于应用程序(数据库、电子表格等)数据的导入和导出以及异构系统之间的数据交换。因为CSV是纯文本文件,不管是什么操作系统和编程语言都是可以处理纯文本的,而且很多编程语言中都提供了对读写CSV文件的支持,因此CSV格式在数据处理和数据科学中被广泛应用。
CSV文件有以下特点:
- 纯文本,使用某种字符集(如ASCII、Unicode、GB2312)等);
- 由一条条的记录组成(典型的是每行一条记录);
- 每条记录被分隔符(如逗号、分号、制表符等)分隔为字段(列);
- 每条记录都有同样的字段序列。
CSV文件可以使用文本编辑器或类似于Excel电子表格这类工具打开和编辑,当使用Excel这类电子表格打开CSV文件时,你甚至感觉不到CSV和Excel文件的区别。很多数据库系统都支持将数据导出到CSV文件中,当然也支持从CSV文件中读入数据保存到数据库中,这些内容并不是现在要讨论的重点。
将数据写入CSV文件
现有五个学生三门课程的考试成绩需要保存到一个CSV文件中,要达成这个目标,可以使用Python标准库中的csv
模块,该模块的writer
函数会返回一个csvwriter
对象,通过该对象的writerow
或writerows
方法就可以将数据写入到CSV文件中,具体的代码如下所示。
import csv
import random
with open('scores.csv', 'w',newline='') as file:
writer = csv.writer(file)
#writer = csv.writer(file, delimiter='|', quoting=csv.QUOTE_ALL)
writer.writerow(['姓名', '语文', '数学', '英语'])
names = ['关羽', '张飞', '赵云', '马超', '黄忠']
for name in names:
scores = [random.randrange(50, 101) for _ in range(3)]
scores.insert(0, name)
writer.writerow(scores)
生成的CSV文件的内容。
姓名,语文,数学,英语
关羽,98,86,61
张飞,86,58,80
赵云,95,73,70
马超,83,97,55
黄忠,61,54,87
需要说明的是上面的writer
函数,除了传入要写入数据的文件对象外,还可以dialect
参数,它表示CSV文件的方言,默认值是excel
。除此之外,还可以通过delimiter
、quotechar
、quoting
参数来指定分隔符(默认是逗号)、包围值的字符(默认是双引号)以及包围的方式。其中,包围值的字符主要用于当字段中有特殊符号时,通过添加包围值的字符可以避免二义性。大家可以尝试将上面第5行代码修改为下面的代码,然后查看生成的CSV文件。
writer = csv.writer(file, delimiter='|', quoting=csv.QUOTE_ALL)
生成的CSV文件的内容。这种模式下excel打开时,不能自动分列。
"姓名"|"语文"|"数学"|"英语"
"关羽"|"88"|"64"|"65"
"张飞"|"76"|"93"|"79"
"赵云"|"78"|"55"|"76"
"马超"|"72"|"77"|"68"
"黄忠"|"70"|"72"|"51"
从CSV文件读取数据
如果要读取刚才创建的CSV文件,可以使用下面的代码,通过csv
模块的reader
函数可以创建出csvreader
对象,该对象是一个迭代器,可以通过next
函数或for-in
循环读取到文件中的数据。
import csv
with open('scores.csv', 'r') as file:
reader = csv.reader(file, delimiter='|')
for data_list in reader:
print(reader.line_num, end='\t') # end='\t'可以避免打印行号后直接换行
#“\t”是指制表符,代表着四个空格,也就是一个tab;它的作用是对齐表格数据的各列,可以在不使用表格的情况下,将数据上下对齐。
for elem in data_list:
print(elem, end='\t')
print()
注意:上面的代码对
csvreader
对象做for
循环时,每次会取出一个列表对象,该列表对象包含了一行中所有的字段。
简单的总结
将来如果大家使用Python做数据分析,很有可能会用到名为pandas
的三方库,它是Python数据分析的神器之一。pandas
中封装了名为read_csv
和to_csv
的函数用来读写CSV文件,其中read_CSV
会将读取到的数据变成一个DataFrame
对象,而DataFrame
就是pandas
库中最重要的类型,它封装了一系列用于数据处理的方法(清洗、转换、聚合等);而to_csv
会将DataFrame
对象中的数据写入CSV文件,完成数据的持久化。read_csv
函数和to_csv
函数远远比原生的csvreader
和csvwriter
强大。
TIPS1:详解Python中字符串前“b”,“r”,“u”,“f”的作用
引自:详解Python中字符串前“b”,“r”,“u”,“f”的作用
1、字符串前加 u
例:u"我是含有中文字符组成的字符串。"
作用:
后面字符串以 Unicode 格式 进行编码,一般用在中文字符串前面,防止因为源码储存格式问题,导致再次使用时出现乱码。
2、字符串前加 r
例:r"\n\n\n\n” 表示一个普通生字符串 \n\n\n\n,而不表示换行了。
作用:
去掉反斜杠的转移机制。
(特殊字符:即那些,反斜杠加上对应字母,表示对应的特殊含义的,比如最常见的”\n”表示换行,”\t”表示Tab等。 )
应用:
常用于正则表达式,对应着re模块。
3、字符串前加 b
例: response = b’Hello World!’ b’ ’ 表示这是一个 bytes 对象
作用:
b" "前缀表示:后面字符串是bytes 类型。
用处:网络编程中,服务器和浏览器只认bytes 类型数据。
如:send 函数的参数和 recv 函数的返回值都是 bytes 类型
附:在 Python3 中,bytes 和 str 的互相转换方式是
str.encode(‘utf-8’)
bytes.decode(‘utf-8’)
4、字符串前加 f
import time
t0 = time.time()
time.sleep(1)
name = ‘processing’
以 f开头表示在字符串内支持大括号内的python 表达式
print(f’{name} done in {time.time() - t0:.2f} s’)
输出:
processing done in 1.00 s
TIPS2:时间复杂度
在计算机科学中,时间复杂性,又称时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。
常数时间
若对于一个算法,的上界与输入大小无关,则称其具有常数时间,记作时间。一个例子是访问数组中的单个元素,因为访问它只需要一条指令。但是,找到无序数组中的最小元素则不是,因为这需要遍历所有元素来找出最小值。这是一项线性时间的操作,或称时间。但如果预先知道元素的数量并假设数量保持不变,则该操作也可被称为具有常数时间。
虽然被称为“常数时间”,运行时间本身并不必须与问题规模无关,但它的上界必须是与问题规模无关的确定值。举例,“如果a > b则交换a、b的值”这项操作,尽管具体时间会取决于条件“a > b”是否满足,但它依然是常数时间,因为存在一个常量t使得所需时间总不超过t。
TIPS3:csv.writer空行问题
python2.x中写入CSV时,CSV文件的创建必须加上’b’参数,即csv.writer(open(‘test.csv’,‘wb’)),不然会出现隔行的现象。网上搜到的解释是:python正常写入文件的时候,每行的结束默认添加’n’,即0x0D,而writerow命令的结束会再增加一个0x0D0A,因此对于windows系统来说,就是两行,而采用’ b’参数,用二进制进行文件写入,系统默认是不添加0x0D的。
而python3.x中换成采用newline=''这一参数来达到这一目的。