一文看懂Python
Python优势
Python拥有很多优点:
- 类型与对象关联,而不是变量。变量可以被赋予任何类型的值,列表也可以包含许多类型的对象。这也意味着通常不需要进行强制类型转换,代码再也不用受制于预先声明的类型了。
- 富有表现力。同样是一行代码,Python可以完成的操作比其他大多数语言都要多。表现力较强的语言,优势十分明显,需要编写的代码越少,项目完成的速度就越快,程序越容易维护和调试。
- 可读性好。代码越容易理解,就越易于调试、维护和修改。Python在这方面的主要优势就是利用缩进,使得代码总是能以一种非常易懂的风格进行格式化。
- 功能齐备。因Python自带了很多函数库,不需要再安装其他库就能真正开始工作。
- 跨平台。Python可以在很多平台上运行,包括Windows、Mac、Linux、UNIX等。
- 开源免费。自始至终,Python就是以开源的方式研发的,并且可以免费获取。任一版本的Python都可自行下载和安装,并可用于开发商业或个人应用。
关于Python运行速度较慢的指摘, 我们只需要记住一点:对绝大多数应用程序而言,现代计算机的计算能力都是过剩的。开发速度比程序运行速度更为重要,而Python程序通常编写速度会快很多。Python非常适合应用程序的快速开发。用Python编写应用程序的时间可能只有用C或Java的五分之一,并且代码行数只有等效C程序的五分之一。另外,用C或C++编写的模块对Python进行扩展也比较容易,程序当中的CPU密集型部分可以交由这些模块来运行。
帮助函数dir()和help()
除了官方帮助文档之外,还可以通过下面两个函数快速查看不熟悉的代码:
- dir():可以列出属于某个对象或模块的全部成员,在查找方法和数据的定义时十分有用。没有给出参数时,它会列出当前的全局变量。
- help():可以列出各种内建函数、模块或模块内方法的使用说明。
一般在不熟悉对象都有哪些方法时,可以先使用dir查看有哪些方法,再使用help查看怎么使用该方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> s = 'aBcDe'
>>> dir(s)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__',
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__',
'__getnewargs__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__',
'__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize',
'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find',
'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal',
'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace',
'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans',
'partition', 'removeprefix', 'removesuffix', 'replace', 'rfind', 'rindex',
'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith',
'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>> help(s.center)
Help on built-in function center:
center(width, fillchar=' ', /) method of builtins.str instance
Return a centered string of length width.
Padding is done using the specified fill character (default is a space).
成为Pythonista的10条建议
- 尊重Python之禅:Python之禅总结了Python成为语言的设计理念,其中
优美胜于丑陋
、简洁胜于复杂
应该始终成为编码的指南。 - 遵守PEP 8:PEP 8是官方的Python风格指南,从代码格式化、变量命名到语言的使用,PEP 8都给出了很好的建议。如果想要写出Python式风格的代码,请对PEP 8烂熟于胸。
- 熟悉文档:Python拥有内容丰富、维护良好的文档集,应该经常去查阅一下。
- 尽可能少写代码:努力让程序短而简单(直至不能更短、更简单为止),尽可能多练习一下这种编码风格。
- 尽可能多读代码:阅读代码比编写代码更为重要。尽可能多读一些Python代码,并与别人讨论读过的代码。
- 熟练掌握生成器和推导式:列表和字典推导式以及生成器表达式是Python式风格编码的重要组成部分。只有几乎不假思索就能写出列表推导,才能成为Python高手。
- 优先采用内置数据结构:在编写自定义类保存数据之前,应该首先考虑一下Python的内置数据结构。Python的多种数据类型几乎可以无限灵活地自由组合,优点是经过了多年调试和优化。
- 采用标准库:如果内置数据结构无法满足需求,接下来应该考虑采用标准库。标准库已经经过了时间的考验,其优化和文档化程度几乎超过了其他任何Python代码。
- 尽可能少写自定义类:仅在必要时才编写自己的类。Python高手往往对自定义类非常谨慎,因为设计良好的类并不是件容易的事,类一旦创建就不得不去做大量测试和调试工作。
- 小心使用框架:框架能提供很多强大的便捷功能,但请小心框架的弊端,你可能会发现自己正在适应框架,而不是让框架适合你。
PEP 8规范
Python Enhancement Proposal 8(PEP 8)中,包含了推荐的代码风格规范。PEP 8的编写者Guido的一个重要观点是,代码被阅读的次数远多于被编写的次数。本指南旨在提高代码的可读性,使各种各样的Python代码能保持风格一致。
完整的PEP 8文档可以参考官方文档,一些重要的代码规范汇总如下:
代码布局
| 场景 | 建议 | | ——– | ————————————————————- | | 缩进 | 每级相差4个空格、不用Tab键 | | 最大行长 | 所有行都应限制在79个字符以内;换行使用\
,且放在运算符之后 | | 空行 | 顶级函数和类之间,两个空行;类内部的各个方法定义之间,1个空行 | | 导入 | 导入语句单独成行,位于文件的顶部,对所有导入都使用绝对包路径 |
在以下二元操作符两侧各放1个空格:赋值、增量赋值、比较、布尔、数学运算符
在以下场合应避免使用多余的空格:
- 紧靠小括号、中括号或大括号符号的内部
- 紧靠逗号、分号或冒号符号的左侧
- 函数参数列表的左括号之前
- 索引或切片操作的左中括号之前
- 函数关键字参数或默认参数值的=两侧
注释
与代码不符的注释还不如不注释。只要代码发生变化,就一定要优先保证更新注释!
注释应该是完整的句子。如果注释是短语或句子,其首个单词应该大写,除非首个单词是以小写字母开头的标识符,永远不要改变标识符的大小写!
如果注释很短,尾部的句点则可以省略。块注释通常由一个或多个段落组成,每个段落都由多个完整句子构成,每个句子都以一个句点结尾。在句子末尾的句点后面,应该加上两个空格。
场景 | 建议 |
---|---|
块注释 | 以# 和一个空格开头、注释内空行使用单独的# 分隔,缩进级别与紧随其后的代码相同 |
行内注释 | 有节制地使用行内注释;和代码末尾至少隔开两个空格,由# 和一个空格开头 |
文档字符串 | 为所有的公有模块、函数、类和方法撰写文档字符串,详细规范参考PEP 257。多行文档字符串结尾的""" 应该自成一行,并且最好是在前面加一行空行。 |
1
2
3
4
5
def kos_root():
"""Return the pathname of the KOS root directory."""
global _kos_root
if _kos_root: return _kos_root
...
1
2
3
4
5
6
7
8
9
10
11
def complex(real=0.0, imag=0.0):
"""Form a complex number.
Keyword arguments:
real -- the real part (default 0.0)
imag -- the imaginary part (default 0.0)
"""
if imag == 0.0 and real == 0.0:
return complex_zero
...
命名规范
| 场景 | 建议 | 示例 | | ———————— | ——————————– | ——————- | | 模块/包名 | 简短、全小写、非必要时不带下划线 | imp、sys | | 类名 | 单词首字母大写 | MyClass | | 函数名、方法名 | 全小写、用下划线增加可读性 | foo()、calc_sum() | | 变量名 | 全小写、用下划线增加可读性 | my_var | | 常量名 | 全大写、单词间下划线分隔 | PI、TAX_RATE | | 受保护的实例属性 | 单下划线前缀 | _leading_underscore | | 私有的实例属性 | 双下划线前缀且不带下划线后缀 | __name | | 类的实例方法的第一个参数 | 使用self
命名,表示该对象本身 | | | 类方法的第一个参数 | 使用cls
命名,表示该类本身 | |
其他建议
| 建议 | 正确示例 | 错误示例 | | —————————————————————- | ————————- | —————————— | | 与None
比较总是用is
或is not
| if x is not None | if x == None: | | 对于字符串、列表,用空序列为False
而不是长度来判断 | if not somelist: | len(somelist)==0、somelist==[] | | 不要使用==
或is
与True
或False
值做比较 | if my_var: | if my_var == True: | | 检查前缀或后缀使用startswith
或endswith
,而不是使用字符串切片 | if foo.startswith(‘bar’): | if foo[:3]==’bar’: |
Python之禅
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren’t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one– and preferably only one –obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea – let’s do more of those!
数据类型和运算符
4种数据类型
Python有4种数值类型:
- 整数型:没有大小限制,仅受限于可用内存的大小。
- 浮点数型:基于C语言的双精度数据类型(64位)实现的,可用小数点或科学计数法表示。
- 复数型:由实部和虚部组合而成,用属性
x.real
可以获得实部,用x.imag
则可获得虚部。 - 布尔型:用
0
或空值(None
、[]
、""
等)表示False,其他任何值都是True。
与C或Java相比,Python的数值有两个优点:整数可为任意大小,两个整数的除法结果是浮点数。
1
2
3
4
ints = [45,-3, 100]
floats = [3.0, 31e12, -6e-2]
complexes = [3+2j, -4-2j, 4.2+6.3j]
bools = [True, False]
内置数值处理函数
| 函数名 | 作用 | | ————- | ———————— | | abs | 取绝对值 | | divmod | 以元组的形式返回商和余数 | | float、int | 类型转换 | | hex、oct、bin | 进制转换 | | max、min | 极大、极小值 | | round | 四舍五入 |
常用运算符
| 操作符 | 功能 | 实例 | 结果 | | ——————– | ——————– | ————– | —– | | +、-、*、/ | 加减乘除运算 | 1+2 | 3 | | // | 整除求商 | 5//2 | 2 | | % | 整除求余 | 5%2 | 1 | | ** | 求幂 | 3**2 | 9 | | round | 四舍五入 | round(3.5) | 4 | | <、<=、==、!=、>、>= | 比较运算符 | 3 >= 2 | True | | is、is not | 比较两个对象是否相同 | x is y | False | | in、not in | 元素是否存在 | 1 in [1, 2, 3] | True | | and、or、not | 逻辑运算符 | 3>1 and 2<0 | False |
数据结构
列表 list
Python列表与Java、C等其他语言的数组非常相似,是对象的有序集合。
与很多其他语言的列表不同,Python的列表可以包含不同类型的元素,列表元素可以是任意的Python对象。
1
2
lis = [1, 2, 3]
lis = [3.2, 'name', [1, 2, 3]]
除此之外,可以利用Python的array模块建立数组。数组元素必须是相同的数据形态,因此占据的内存空间较少。列表而可以拥有多个数据形态的数据,所需内存空间较大。
列表的索引机制
索引为0将返回列表的第一个元素,依此类推。如果索引为负数,表示从列表末尾开始计数的位置,其中-1是列表的最后位置。
切片(slice)操作:Python支持一次提取或赋值一整个子列表,用list[index1:index2]
提取index1
(含)和上限至index2
(不含)之间的所有数据项,并放入一个新列表中。
- 如果
index1 > index2
,则返回一个空列表; - 如果
index1
或index2
其中一个省略,则相当于从最开始开始或者到末尾截止; - 如果
index1
和index2
全部省略,则相当于从头至尾创建一个新列表,即列表复制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> x = ["first", "second", "third", "fourth"]
>>> x[0]
'first'
>>> x[-1]
'fourth'
>>> x[1:3]
['second', 'third']
# 列表复制,对复制后的列表进行更改不会影响原列表
>>> y = x[:]
>>> y[0] = "1st"
>>> y
['1st', 'second', 'third', 'fourth']
>>> x
['first', 'second', 'third', 'fourth']
修改列表
除了提取列表元素,使用列表索引语法还可以修改列表。
1
2
3
4
>>> x = [1, 2, 3, 4]
>>> x[1] = "two"
>>> x
[1, 'two', 3, 4]
切片操作也可以这样使用,例如listA[index1:index2]=listB
,可以用listB
的值替换listA
的值,如果listB
的长度多于或少于[index1:index2]
,listA
会自动调节长度。利用切片赋值操作,可以实现很多功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在列表末尾追加列表
>>> listA = [1, 2, 3, 4]
>>> listB = [5, 6, 7]
>>> listA[len(listA):] = listB
>>> listA
[1, 2, 3, 4, 5, 6, 7]
# 在列表开头插入列表
>>> listC = [-2, -1, 0]
>>> listA[:0] = listC
>>> listA
[-2, -1, 0, 1, 2, 3, 4, 5, 6, 7]
# 移除列表元素
>>> listA[1:-1] = []
>>> listA
[-2, 7]
除了切片操作,列表也提供了很多内置方法来修改列表中的元素:
函数名 | 作用 | 实例 |
---|---|---|
append | 向列表末尾添加单个元素 | lis.append(10) |
extend | 向列表末尾添加另一个列表 | lis.extend([1,2,3,4]) |
insert | 在指定位置插入列表元素 | lis.insert(2, ‘hello’) |
del | 删除列表数据项或切片 | del x[:3] |
remove | 查找给定值的第一个实例,并将该值从列表中删除 | lis.remove(‘hello’) |
reverse | 将列表逆序排列 | lis.reverse() |
如果remove
找不到要删除的的值,就会引发错误。可以用Python的异常处理机制来捕获错误,也可以在做remove
之前,先用in
检查一下要删除的值是否存在,以避免错误的发生。
排序列表
Python内置的sort
方法可以对任何对象进行排序,但是有一点需要注意:sort
用到的默认键方法要求列表中所有数据项均为可比较的类型。如果列表同时包含数字和字符串,那么使用sort
方法将会引发异常。
1
2
3
4
>>> x = [3, 8 ,4, 0, 1]
>>> x.sort()
>>> x
[0, 1, 3, 4, 8]
sort
方法带有可选的reverse
参数,当reverse=True
时可以实现逆向排序,可以用自定义的键函数来决定列表元素的顺序。定义了键函数之后,就可以通过关键字key
将键函数传递给sort
方法。
例如,下面假设需要按照每个单词的字符数对单词列表进行排序,而不是Python通常的词典顺序。因此需要编写一个函数,用于返回需要排序的键,并将该函数与sort
方法一起使用。该函数接受一个参数,并返回供sort
函数使用的键。
1
2
3
4
5
6
7
8
9
10
11
12
def compare_num_of_chars(string1):
return len(string1)
>>> word_list = ['Python', 'is', 'better', 'than', 'C']
# 默认排序结果
>>> word_list.sort()
['C', 'Python', 'better', 'is', 'than']
# 自定义排序结果
>>> word_list.sort(key=compare_num_of_chars)
['C', 'is', 'than', 'Python', 'better']
自定义排序非常有用,但如果性能是关键需求,可能会比不上默认排序速度。键函数通常对性能影响很小,但如果键函数特别复杂,则影响可能会超出预期,尤其当涉及数十万或数百万个元素排序时。
Python还内置有sorted()
函数,能够从任何可迭代对象返回有序列表。和列表的sort
方法一样,sorted()
函数同样也用到了参数key
和reverse
:
1
2
3
4
>>> x = [4, 3, 1, 2]
>>> y = sorted(x)
>>> y
[1, 2, 3, 4]
其他常用的列表操作
| 列表操作 | 作用 | 实例 | | ——– | —————————- | ——————- | | len | 返回列表长度 | len(lis) | | + | 将两个列表拼接在一起 | list1 + list2 | | * | 将列表复制多份 | lis = [1, 2, 3] * 3 | | min、max | 返回列表的最大、最小元素 | max(lis) | | index | 返回某值在列表中的位置 | lis.index(‘x’) | | count | 对某值在列表中出现的次数计数 | lis.count(‘x’) | | sum | 对列表中的数据进行求和 | sum(lis) | | in | 返回某数据项是否为列表的元素 | ‘x’ in lis |
嵌套列表和深复制
列表可以嵌套。嵌套列表的一种用途是表示二维矩阵。矩阵的成员可以通过二维索引来引用。
1
2
3
4
5
>>> m = [[0, 1, 2], [10, 11, 12], [20, 21, 22]]
>>> m[0]
[0, 1, 2]
>>> m[0][1]
1
通过全切片x[:]
可以得到列表的副本,用+
或*
操作符(如x+[]
或x*1
)也可以得到列表的副本。但它们的效率略低于使用切片的方法。
这3种方法都会创建所谓的浅复制(shallow copy),大多数情况下这也能够满足需求了。
但如果列表中有嵌套列表,那就可能需要深复制(deep copy)。
- 对于不可变数据而言(常数、元组等),浅复制、深复制与赋值(=)概念相同,变量具有相同地址,所以某一个数据更改,所有变量数据也会更改。
- 对于可变数据而言(列表等),浅复制的对象是独立的对象,但是它们的子对象元素是指向同一对象,也就是对象的子对象会连动。
深复制可以通过copy
模块的deepcopy
函数来得到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import copy
>>> a = [1,2,[3,4]]
>>> b = a[:]
>>> id(a), id(b)
(1906619971776, 1906620404736) # 浅复制的对象是独立的对象,地址发生改变
>>> a.append(5)
>>> a, b
([1, 2, [3, 4], 5], [1, 2, [3, 4]]) # a发生改变不会影响b
>>> a[2].append(6)
>>> a, b
([1, 2, [3, 4, 6], 5], [1, 2, [3, 4, 6]]) # a的子元素发生改变会影响b
>>> import copy
>>> b = copy.deepcopy(a)
>>> a[2].append(6)
>>> a, b
([1, 2, [3, 4, 6]], [1, 2, [3, 4]]) # 深复制下,a的子元素发生改变也不会影响b
元组 tuple
元组是与列表非常相似的数据结构。但是元组只能创建,不能修改。元组与列表非常像,但元组具有列表无法实现的重要作用,如用作字典的键。
1
>>> tup = ('a', 'b', 'c')
单个元素的元组应加上逗号
1
2
3
4
5
6
7
>>> x, y = 3, 4
>>> (x + y) # 此行代码将把x和y相加
7
>>> (x + y,) # 跟了逗号就意味着,圆括号是用来标识元组的
(7,)
>>> () # 用成对的空的圆括号创建一个空元组
()
元组的常见操作
除了无法修改之外,元组的操作和列表基本类似。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> tup = ('a', 'b', 'c')
>>> tup[2]
'c'
>>> tup[1:]
('b', 'c')
>>> len(tup)
3
# 元组的拼接和复制
>>> tup + tup
('a', 'b', 'c', 'a', 'b', 'c')
>>> 2 * tup
('a', 'b', 'c', 'a', 'b', 'c')
>>> tup[:]
('a', 'b', 'c')
需要注意的是,元组本身不能被修改,但假如包含了可变对象(如列表或字典),而且这些对象被各自赋值了变量,就可以对可变对象实现修改。包含可变对象的元组,是不允许作为字典的键使用的。
元组的打包和拆包
Python允许元组出现在赋值操作符的左侧,这时元组中的变量会依次被赋予赋值操作符右侧元组的元素值。
1
2
3
4
5
6
7
8
9
10
11
>>> (one, two, three, four) = (1, 2, 3, 4)
>>> one
1
# 以上示例还可以写得更加简洁一些,因为在赋值时即使没有圆括号,Python也可以识别出元组。
>>> one, two, three, four = 1, 2, 3, 4
>>> one
1
# 采用这种技巧,交换两个变量的值就变得十分简便。
var1, var2 = var2, var1
为了进一步方便使用,Python还支持扩展的拆包特性,允许带*
的元素接收任意数量的未匹配元素。带星号的元素会把多余的所有数据项接收为列表。如果没有多余的元素,则带星号的元素会收到空列表。
1
2
3
4
5
6
7
8
9
10
>>> x = (1, 2, 3, 4)
>>> a, b, *c = x
>>> a, b, c
(1, 2, [3, 4])
>>> *a, b, c = x
>>> a, b, c
([1, 2], 3, 4)
>>> a, b, c, d, *e = x
>>> a, b, c, d, e
(1, 2, 3, 4, [])
列表和元组的相互转换
1
2
3
4
>>> list((1, 2, 3, 4))
[1, 2, 3, 4]
>>> tuple([1, 2, 3, 4])
(1, 2, 3, 4)
此外,利用list
函数可以很容易就能将字符串拆分为字符。
1
2
>>> list("Hello")
['H', 'e', 'l', 'l', 'o']
集合 set
集合是一组对象的无序集。如果主要关心的是成员是否属于集合、是否唯一,那么集合就比较有用了。与字典键类似,集合中的项必须是不可变的、可散列的。因此,整数、浮点数、字符串和元组可以作为集合的成员,但列表、字典和集合本身不可以。
从序列创建集合需要使用set函数,在由序列生成集合时,重复的元素将会自动被移除。
1
2
3
>>> x = set([1, 2, 3, 1, 3, 5])
>>> x
{1, 2, 3, 5}
集合的常见操作
集合还支持很多特有的操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> x = set([1, 2, 3, 1, 3, 5])
# 向集合中添加元素
>>> x.add(6)
>>> x
{1, 2, 3, 5, 6}
# 移除集合中的元素
>>> x.remove(5)
>>> x
{1, 2, 3, 6}
# 判断元素是否在集合中
>>> 1 in x
True
>>> 4 in x
False
# 并集、交集、对称差运算
>>> y = set([1, 7, 8, 9])
>>> x | y
{1, 2, 3, 6, 7, 8, 9}
>>> x & y
{1}
>>> x ^ y # 集合的对称差:属于其中一个但不同时属于两个集合的元素
{2, 3, 6, 7, 8, 9}
不可变集合
因为集合是可变的,所以不能用作其他集合的成员。为了让集合本身也能够成为集合的成员,Python提供了另一种集合类型frozenset
,它与集合很相像,但是创建之后就不能更改了,因此可以作为其他集合的成员:
1
2
3
4
5
6
7
>>> x = set([1, 2, 3, 1, 3, 5])
>>> z = frozenset(x)
>>> z frozenset({1, 2, 3, 5})
>>> z.add(6)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'add'
字典 dictory
列表中的值可以通过整数索引进行访问,索引表示了给定值在列表中的位置。列表中存储的值隐含了按照在列表中的位置排序,因为访问这些值的索引是连续的整数。这种顺序可能会被忽略,但需要时就可以用到。
字典中的值value
通过键key
进行访问,键可以是整数、字符串或其他Python对象,同样表示了给定值在字典中的位置。存储在字典中的值相互之间没有隐含的顺序关系,因为字典的键不只是数字。如果用字典的时候同时还需要考虑条目的顺序(指加入字典的顺序),那么可以使用有序字典。
创建字典
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 直接创建空字典
dic = {}
dic = dict()
# 直接赋值创建字典
dic = {'name': 'Jack', 'age': 18, 'height': 180}
# 通过关键字dict和关键字参数创建
dic = dict(name='Jack', age=18, height=180)
# 通过关键字dict和数组创建
lis = [('name', 'Jack'), ('age', 18), ('height', 180)]
dic = dict(lis)
# 过关键字dict和zip创建(需要重点掌握!!!)
dic = dict(zip('abc', [1, 2, 3]))
# 通过推导公式创建
dic = {i: i ** 2 for i in range(1, 5)}
zip()
是一个内建函数,参数内容主要是2个或更多个可迭代对象,如果有多个对象(例如列表或元组),可以用zip()
将多个对象打包成zip对象,然后未来视需要将此zip对象转成列表或其他对象,例如元组。在列表打包时,如果有一个列表比较短,当比较短的列表打包到最后一个元素时,这个打包工作就会结束。
字典的常见操作
| 操作 | 作用 | 实例 | | —————- | ————————————————————————– | —————————– | | len() | 返回字典的条目数量 | len(dic) | | keys(), values() | 获取字典中的所有键/值 | dic.keys() | | items() | 以元组形式获取字典中的所有键值对 | dic.items() | | del | 删除字典中的键值对 | del dic[‘name’] | | in | 检查字典中key是否存在 | ‘age’ in dic.keys() | | get() | 如果字典中包含该键,则返回与键关联的值。如果不包含则返回None
或第二个参数 | dic.get(‘hobby’, ‘not exist’) | | copy() | 获得字典的副本(浅复制) | dic.copy() | | update() | 将两个字典的条目合并 | dic1.update(dic2) |
如果要访问的键在字典中不存在,则会被Python视为出错,可以用in
先检测一下字典中是否存在该键。
熟练使用字典的特性,在某些场景下可以非常高效,比如对一段文本中的单词进行计数:
1
2
3
4
sample_string = "To be or not to be"
occurrences = {}
for word in sample_string.split():
occurrences[word] = occurrences.get(word, 0) + 1 # 对每个单词出现的次数进行累加
可用作字典键的对象
任何不可变(immutable)且可散列(hashable)的Python对象,都可被用作字典的键。
- 不可变:对象无法被修改。
- 可散列:对象必须带有散列值(由__hash__方法提供),并且在值的整个生命周期内保持不变。
稀疏矩阵
矩阵是指数字的二维网格,通常在教科书中会在两边加上方括号表示,这种矩阵的标准表示方法就是使用二维列表。
1
2
3
matrix = [[3, 0, -2, 11], [0, 9, 0, 0], [0, 7, 0, 0], [0, 0, 0, -5]]
ele = matrix[0][1] # 通过行号和列号来访问矩阵中的元素
在天气预报之类的应用中,矩阵往往十分庞大,每条边有数千个元素,这就意味着矩阵总共有数百万个元素,而且这种矩阵中很多元素常常都为0。在某些应用中,除少量元素之外,其他矩阵元素都可能为0。为了节省内存,往往会采用某种形式,实际只存储其中的非0元素。这种矩阵被称为稀疏矩阵。
用元组索引的字典可以轻松实现稀疏矩阵。例如,上面的矩阵就可以表示如下:
1
2
3
4
5
6
7
8
9
10
matrix = {(0, 0): 3, (0, 2): -2, (0, 3): 11,
(1, 1): 9, (2, 1): 7, (3, 3): -5}
# 通过行号和列号组成的元祖来访问矩阵中的元素
if (0, 1) in matrix:
element = matrix[(0, 1)]
else:
element = 0 # 元祖不存在,说明元素为0
element = matrix.get((0, 1), 0) # 使用get方法来访问元素,代码更加高效和简洁
将字典用作缓存
字典的另一大作用是可以作为缓存(cache),也就是保存计算结果的数据结构,避免重复计算。
假设有一个sole
函数,参数为3个整数且所有的参数组合大约有200种,函数在执行完某些相当耗时的计算会返回结果。
如果函数被调用数万次,则程序运行就会十分缓慢。但我们可以发现,程序在运行时同一个参数组合sole(12, 20, 6)
可能会被调用50次或以上。因此通过消除参数相同时的重复计算,就可以节省大量时间。
1
2
3
4
5
6
7
8
9
10
11
sole_cache = {}
def sole(m, n, t):
if (m, n, t) in sole_cache:
# 只要参数组合是曾经计算过的,就不会再次计算,而是直接返回保存过的结果
return sole_cache[(m, n, t)]
else:
# 如果参数组合没有被计算过,则执行较为耗时的计算任务
result = complex_exec(m, n, t)
# 将结果保存到缓存区
sole_cache[(m, n, t)] = result
return result
字符串
Python中可以将字符串看作是一系列字符,因此可以使用索引和切片语法进行操作。
1
2
3
4
>>> x = "Goodbye\n"
>>> x = x[:-1]
>>> x
'Goodbye'
但是字符串并不是字符列表。字符串和列表之间最明显的区别就是,字符串不可修改。在上面的例子中,是新建了原字符串的一个切片,而不是直接在原字符串上做修改,如果直接修改则会报错。
1
2
3
4
>>> x[-1] = ''
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
这是Python的一个基本限定,目的是为了提高效率。
特殊字符和转义序列
以反斜杠\
开头,用于表示其他字符的字符序列,被称为转义序列(escapesequence)。转义序列通常用来表示特殊字符,也就是这种字符没有标准的用单字符表示的可打印格式(如制表符和换行符)。
ASCII字符集是Python使用的字符集,也几乎是所有计算机采用的标准字符集。在ASCII字符集中定义了相当多的特殊字符,通过数字格式的转义序列就可以获取到这些特殊字符。
转义序列 | 代表的字符 |
---|---|
", ' | 双引号和单引号 |
\ | 反斜杠 |
\a | 振铃符 |
\b | 退格符 |
\f | 换页符 |
\n | 换行符 |
\r | 回车符(与\n不同) |
\t | 制表符(Tab) |
\v | 纵向制表符 |
在字符串中,可以用与ASCII字符对应的八进制或十六进制转义序列来包含任何ASCII字符。
- 八进制转义序列是反斜杠
\
后跟3位八进制数,这个八进制数对应的ASCII字符将会被八进制转义序列替代。 - 十六进制转义序列不是用
\
作为前缀,而是用\x
,后跟任意位数的十六进制数。
例如,在ASCII表中,字符m
转换为十进制值为109,转换成八进制值就是155,转换成十六进制值则为6D:
1
2
3
4
5
6
7
8
9
10
11
12
>>> 'm'
'm'
>>> '\155'
'm'
>>> '\x6D'
'm'
# 对于换行符\n,八进制值为012,十六进制值为0A
>>> '\012'
'\n'
>>> '\x0A'
'\n'
Python 3的字符串都是Unicode字符串,因此几乎能够包含所有语言的全部字符。使用上面提到的数字格式或Unicode名称,就可以转义任何Unicode字符:
1
2
3
4
5
>>> unicode_a_with_acute = '\N{LATIN SMALL LETTER A WITH ACUTE}' # 用Unicode名称转义
>>> unicode_a_with_acute
'á'
>>> "\u00E1" # 用数字格式转义,前缀为\u
'á'
字符串方法
大部分字符串方法都是通过点操作符.
依附于它们操作的字符串对象,如x.upper()
方法。因为字符串是不可变的,所以字符串方法只能用来获取返回值,不能以任何方式修改其依附的字符串对象。
字符串拼接和分割 join split
join
方法以字符串列表为参数,将字符串连在一起形成一个新字符串,各元素之间插入调用者字符串。
虽然用+
拼接字符串很有用,但要将大量字符串拼接成一个字符串,其效率并不高。因为每次应用+
都会创建一个新的字符串对象。更好的选择是采用join
方法。
1
2
3
4
5
>>> "::".join(["Separated", "with", "colons"])
'Separated::with::colons'
>>> "".join(["Separated", "by", "nothing"])
'Separatedbynothing'
split
方法返回字符串中的子字符串列表,默认使用空白符作为拆分字符串的分隔符,但可以用可选参数来更换分隔符。
1
2
3
4
5
6
7
>>> x = "You\t\t can have tabs\t\n \t and newlines \n\n " \
... "mixed in"
>>> x.split()
['You', 'can', 'have', 'tabs', 'and', 'newlines', 'mixed', 'in']
>>> x = "Mississippi"
>>> x.split("ss")
['Mi', 'i', 'ippi']
此外,通过给split
方法传入第二个可选参数来指定生成结果时执行拆分的次数。
1
2
3
4
5
>>> x = 'a b c d'
>>> x.split(' ', 1)
['a', 'b c d']
>>> x.split(' ', 9)
['a', 'b', 'c', 'd']
一般在处理其他程序生成的文本文件时,会大量使用split
和join
方法。如果自己编写的程序需要生成更多标准格式的输出文件时,更推荐采用Python标准库中的csv
和json
模块。
将字符串转换为数值 int
利用函数int
和float
,可以将字符串分别转换为整数或浮点数。如果字符串无法转换为指定类型的数值,那么这两个函数将会引发ValueError异常。此时可以通过eval
函数来解决。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> float("12.34")
12.34
>>> int("12.34")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '12.34'
>>> int(eval("12.34"))
12
>>> int("101001",2)
41
>>> int("ff",16)
255
去除多余的空白符 strip
Python有3个用于去除字符串中空白符的方法:
strip
将返回与原字符串相同的新字符串,只是首尾的空白字符都会被移除。lstrip
:移除的是原字符串左边的空白符rstrip
:移除的是原字符串右边的空白符
1
2
3
4
5
6
7
>>> x = " Hello, World\t\t "
>>> x.strip()
'Hello, World'
>>> x.lstrip()
'Hello, World\t\t '
>>> x.rstrip()
' Hello, World'
在上述例子中,制表符也被当作是空白符。在不同操作系统中,空白符的确切定义可能会不一样,可以查看string.whitespace
常量来弄清楚Python对空白符的定义。
上面3个方法可以附带一个参数,这个参数包含了需要移除的字符。
1
2
3
4
5
>>> x = "www.python.org"
>>> x.strip("w") # 删除所有的w字符
'.python.org'
>>> x.strip(".gorw") # 删除所有的.、g、o、r、w字符
'python'
字符串搜索 find
基础的字符串搜索方法有4个,即find
、rfind
、index
和rindex
,它们比较类似。还有3个相关的方法,即count
、startswith
、endswith
。可以搭配正则表达式可以实现非常强大的效果。
find
方法有一个必填参数,即需要搜索的子字符串。find
方法将会返回子字符串第一个实例的首字符在调用字符串对象中的位置,如果未找到子串则返回-1
:
1
2
3
4
5
>>> x = "Mississippi"
>>> x.find("ss")
2
>>> x.find("zz")
-1
find
方法还可以带一或两个可选参数start
和end
,用于制定搜索的范围:
1
2
3
4
5
>>> x = "Mississippi"
>>> x.find("ss", 3)
5
>>> x.find("ss", 0, 3)
-1
rfind
方法的功能与find
方法几乎完全相同,但是从字符串的末尾开始搜索,返回的是子字符串在字符串中最后一次出现时的首字符位置。同样也可以使用可选参数start
和end
。
1
2
3
>>> x = "Mississippi"
>>> x.rfind("ss")
5
index
和rindex
方法分别与find
和rfind
功能完全相同,但是有一点不同:当在字符串中找不到子字符串时,不会返回-1
,而是会引发ValueError。
count
方法的用法,与上面4个函数完全相同,但是返回的是给定子字符串在给定字符串中不重叠出现的次数:
1
2
3
>>> x = "Mississippi"
>>> x.count("ss")
2
startswith
和endswith
方法对简单搜索来说十分有用,可用于确认检查的字符串是否位于行首或行尾。
1
2
3
4
5
6
7
>>> x = "Mississippi"
>>> x.startswith("Miss")
True
>>> x.endswith("pi")
True
>>> x.endswith(("i", "u"))
True
字符串修改 replace
字符串是不可变的,但字符串对象有几个方法可以对该字符串执行操作并返回新字符串,新字符串是原字符串修改后的版本。大多数情况下,这种做法达到了与直接修改相同的效果。
不过一般情况下,更推荐使用re模块的字符串替换函数,更加灵活。
replace
方法可以将字符串中的子字符串(第一个参数)全部替换为另一个字符串(第二个参数)。
1
2
3
>>> x = "Mississippi"
>>> x.replace("ss", "+++")
'Mi+++i+++ippi'
maketrans
和translate
可以配合起来使用,将字符串中的多个字符转换为其他字符。不过一般很少用到。
尽管生成新字符串的处理方式(原字符串保持不变)很多时候也很有用,但有时需要能像处理字符列表一样处理字符串。这时可将字符串转换为字符列表,按需处理完成后再将字符列表结果转换回字符串:
1
2
3
4
5
6
7
>>> text = "Hello, World"
>>> wordList = list(text)
>>> wordList[6:] = [] # 移除逗号之后的所有字符
>>> wordList.reverse()
>>> text = "".join(wordList)
>>> print(text)
olleH
字符串的格式化输出
声明三个变量用于演示:姓名(string)、年龄(int)、身高(float)
1
2
3
>>> name = "James"
>>> age = 18
>>> height = 1.85
%格式化
优点:在简单练习中,表达比较直观,易于掌握。
缺点:随着参数数量的增加,格式输入会逐渐变得繁琐。
1
2
3
4
5
>>> print('我是:%s, 年龄:%d, 身高:%fm' % (name,age,height))
我是:James, 年龄:18, 身高:1.850000m
>>> print('我是:%s, 年龄:%d, 身高:%.2fm' % (name,age,height))
我是:James, 年龄:18, 身高:1.85m
format格式化
优点:可以根据编号设置单个参数多次输出,填充方式比较灵活。
缺点:随着参数数量的增加,输入长度会逐渐增加,当有大量参数时,同样比较繁琐。
1
2
>>> print('我是:{}, 年龄:{}, 身高:{}m'.format(name,age,height))
我是:James, 年龄:18, 身高:1.85m
f-string格式化(推荐)
优点:格式化的方式较前两种更加直观,效率也较前两个高一些。
缺点:新的格式化方式,有些语句格式较前两种复杂一些。
可以是直接的数值运算,也可以是变量赋值后运算,也可以使用普通函数、lambda函数作为参数
1
2
3
4
5
6
7
8
>>> print(f'我是:{name}, 年龄:{age}, 身高:{height}m')
我是:James, 年龄:18, 身高:1.85m
>>> print(f'我是:{name.lower()}, 年龄:{age*10}, 身高:{height}m')
我是:james, 年龄:180, 身高:1.85m
>>> print(f'我是:{name:^10s}, 年龄:{age:^10d}, 身高:{height:^10.1f}m')
我是: James , 年龄: 18 , 身高: 1.9 m
bytes对象
bytes对象与string对象比较类似,但有一个重要区别:string对象是Unicode字符组成的不可变序列,而bytes对象是值从0到256的整数序列。如果需要处理二进制数据,例如,从二进制数据文件中读取数据时,bytes对象是必需的。
bytes对象看起来像string,但不能像string对象那样使用,也不能与string对象拼接。这点非常重要。
1
2
3
4
5
6
7
8
9
>>> unicode_a_with_acute = '\N{LATIN SMALL LETTER A WITH ACUTE}'
>>> unicode_a_with_acute
'á'
>>> xb = unicode_a_with_acute.encode() # 将普通字符串转换为bytes对象
>>> xbb # 编码为bytes对象后,字符成了两个字节
'\xc3\xa1'
>>> xb.decode() # 将bytes对象转换回字符串
'á'
流程控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# while循环
while condition:
body
# for循环
for item in sequence:
body
# if语句
if condition1:
body1
elif condition2:
body2
else:
body3
流程控制常用生成器函数
range函数
将range
函数和len
函数结合起来使用,生成供for
循环使用的索引序列。
1
2
3
4
x = [1, 3, -7, 4, 9, -5, 4]
for i in range(len(x)):
if x[i] < 0:
print("Found a negative number at index ", i)
enumerate函数
通过组合使用元组拆包和enumerate
函数,可以实现同时对数据项及其索引进行循环遍历。用法与range
函数类似,优点是代码更清晰、更易理解。
1
2
3
4
x = [1, 3, -7, 4, 9, -5, 4]
for i, n in enumerate(x):
if n < 0:
print("Found a negative number at index ", i)
zip函数
在循环遍历之前将两个或以上的可迭代对象合并在一起,有时候会很有用。zip
函数可以从一个或多个可迭代对象中逐一读取对应元素,并合并为元组,直至长度最短的那个可迭代对象读取完毕:
1
2
3
4
5
>>> x = [1, 2, 3, 4]
>>> y = ['a', 'b', 'c']
>>> z = zip(x, y)
>>> list(z)
[(1, 'a'), (2, 'b'), (3, 'c')]
列表和字典推导式
利用for
循环遍历列表、修改或选中某个元素、新建列表或字典,这些都是十分常见的用法。
1
2
3
4
x = [1, 2, 3, 4]
x_squared = []
for item in x:
x_squared.append(item * item)
因此Python为这种操作提供了特殊的快捷写法,称为推导式(comprehension)。
1
2
x = [1, 2, 3, 4]
x_squared = [item * item for item in x]
这里甚至可以用if
语句来筛选列表的项:
1
2
x = [1, 2, 3, 4]
x_squared = [item * item for item in x if item > 2]
函数
定义函数
1
2
3
4
5
6
7
def fact(n):
""" Return the factorial of the given number. """
r = 1
while n > 0:
r = r * n
n = n - 1
return r
函数定义下方的注释是可选的文档字符串(docstring),可通过fact.__doc__
读取其值。文档字符串用于描述函数对外表现出来的功能及所需的参数,通常用3重引号包围起来,以便能跨越多行。标准的多行文档字符串写法,是在第一行中给出函数的概述,第二行是空行,然后是其余的详细信息。
函数参数
在Python中,最简单的函数传参方式就是按位置给出。在函数定义的第一行中,可以为每个参数指定变量名。
1
2
3
4
5
6
7
8
9
def power(x, y):
r = 1
while y > 0:
r = r * x
y = y - 1
return r
power(3, 3)
power(x=1, y=2)
变长参数
Python函数也可以定义为参数数量可变的形式,定义方式有两种。
- 位置参数数量不定时
当函数的最后一个参数名称带有*
前缀时,在一个函数调用中所有多出来的非关键字传递参数将会合并为一个元组赋给该参数。
1
2
3
4
5
6
7
8
9
10
11
12
def maximum(*numbers):
if len(numbers) ==0:
return None
else:
maxnum = numbers[0]
for n in numbers[1:]:
if n > maxnum:
maxnum = n
return maxnum
>>> maximum(1, 5, 9, -2, 2)
9
- 关键字传递参数数量不定时
如果参数列表的最后一个参数前缀为**
,那么所有多余的关键字传递参数将会被收入一个字典对象中。字典的键为多余参数的关键字,字典的值为参数本身。
1
2
3
4
5
6
7
8
9
10
def example_fun(x, y, **other):
print("x: {0}, y: {1}, keys in 'other': {2}".format(x, y, list(other.keys())))
other_total = 0
for k in other.keys():
other_total = other_total + other[k]
print("The total of values in 'other' is {0}".format(other_total))
>>> example_fun(2, y="1", foo=3, bar=4)
x: 2, y: 1, keys in 'other': ['foo', 'bar']
The total of values in 'other' is 7
混合使用多种参数传递方式的一般规则是,先按位置传递参数,接着是命名参数,然后是带单个*
的数量不定的位置传递参数,最后是带**
的数量不定的关键字传递参数。
将可变对象用作函数参数
函数参数传递的是对象的引用。对于不可变对象(如元组、字符串和数值),对参数的操作不会影响函数外部的代码。但如果传入的是可变对象(如列表、字典或类的实例),则对该对象做出的任何改动都会改变该参数在函数外引用的值,函数内部对参数的重新赋值则不受影响。
1
2
3
4
5
6
7
8
9
10
11
def f(n, list1, list2):
list1.append(3)
list2 = [4, 5, 6]
n = n + 1
>>> x = 5
>>> y = [1, 2]
>>> z = [4, 5]
>>> f(x, y, z)
>>> x, y, z
(5, [1, 2, 3], [4, 5])
局部变量、非局部变量和全局变量
1
2
3
4
5
6
7
def fact(n):
""" Return the factorial of the given number. """
r = 1
while n > 0:
r = r * n
n = n - 1
return r
变量r和n对于fact函数的任何调用都是局部(local)的,在函数执行期间,它们的变化对函数外部的任何变量都没有影响。函数参数列表中的所有变量,以及通过赋值(如fact函数中的r =1)在函数内部创建的所有变量,都是该函数的局部变量。
global
关键字用于在函数内部声明变量为全局变量。当需要在一个函数内修改全局作用域中的变量时,就需要使用global
关键字。
1
2
3
4
5
6
7
8
x = 10 # 全局变量
def modify_global():
global x
x = 20 # 修改全局变量
modify_global()
print(x) # 输出: 20
nonlocal
关键字用来在函数或其他作用域中使用外层(非全局)变量。它主要用在嵌套函数中,当需要修改嵌套作用域内的变量时。
1
2
3
4
5
6
7
8
9
10
11
def outer():
y = 10 # 外层函数中的局部变量
def inner():
nonlocal y
y = 20 # 修改外层函数中的局部变量
inner()
return y
print(outer()) # 输出: 20
将函数赋给变量
与其他Python对象一样,函数也可以被赋值。
1
2
3
4
5
6
7
8
9
10
11
def f_to_kelvin(degrees_f):
return 273.15 + (degrees_f - 32) * 5 / 9
def c_to_kelvin(degrees_c):
return 273.15 + degrees_c
abs_temperature = f_to_kelvin # 将f_to_kelvin函数赋给变量
abs_temperature(32) # 输出:273.15
abs_temperature = c_to_kelvin # 将c_to_kelvin函数赋给变量
abs_temperature(0) # 输出:273.15
函数可以被放入列表、元组或字典中
1
2
3
t = {'FtoK': f_to_kelvin, 'CtoK': c_to_kelvin}
t['FtoK'](32) # 输出:273.15
t['CtoK'](0) # 输出:273.15
常用匿名函数
lambda
表达式:匿名的小型函数,用来快速地在行内完成函数定义。map
函数:针对可迭代对象中的所有元素生成函数调用的结果。filter
函数:针对可迭代对象的元素调用特定函数,程序会选出函数结果为真的元素并生成相应的结果。
通过lambda
编写求和函数:
1
2
3
4
5
6
# lambda版本
add2 = lambda x, y: x + y
#常规版本
def _(x, y):
return x + y
通过lambda
和map
将列表中的所有元素加倍:
1
2
3
my_list = [1, 2, 3, 4, 5, 6]
list(map(lambda x: x * 2, my_list))
输出:[2, 4, 6, 8, 10, 12]
通过lambda
和filter
从列表中提取特定的元素:
1
2
3
my_list = [1, 2, 3, 4, 5, 6]
list(filter(lambda x: x > 3, my_list))
输出:[4, 5, 6]
生成器
生成器(generator)函数是一种特殊的函数,可用于定义自己的迭代器(iterator)。在定义生成器函数时,用关键字yield
返回每一个迭代值。当没有可迭代值,或者遇到空的return
语句或函数结束时,生成器函数将停止返回值。与普通的函数不同,生成器函数中的局部变量值会保存下来,从本次调用保留至下一次调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def three():
x = 0
while x < 3:
print("inside generator, x = ", x)
yield x
x += 1
for i in three():
print("outside generator, x = ", x)
inside generator, x = 0
outside generator, x = 0
inside generator, x = 1
outside generator, x = 1
inside generator, x = 2
outside generator, x = 2
装饰器
因为函数是Python的一级对象(first-class),所以能被赋给变量。函数也可以作为参数传递给其他函数,还可作为其他函数的返回值回传。例如,可以编写一个函数,它把其他函数作为参数,并将这个参数嵌入另一个执行相关操作的新函数中,然后返回这个新函数。这个新的函数可用于替换原来的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def decorate(func):
print("in decorate function, decorating", func.__name__)
def wrapper_func(*args):
print("Executing", func.__name__)
return func(*args)
return wrapper_func
def myfunction(parameter):
print(parameter)
>>> myfunction = decorate(myfunction)
in decorate function, decorating myfunction
>>> myfunction("hello")
Executing myfunction
hello
装饰器(decorator)就是上述过程的语法糖(syntactic sugar),只增加一行代码就可以将一个函数包装到另一个函数中去。效果与上述代码完全相同,不过最终的代码则更加清晰易懂。装饰器由两部分组成:
- 先定义用于装饰其他函数的装饰器函数;
- 然后立即在被装饰函数的定义前面,加上
@
和装饰器函数名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def decorate(func):
print("in decorate function, decorating", func.__name__)
def wrapper_func(*args):
print("Executing", func.__name__)
return func(*args)
return wrapper_func
@decorate
def myfunction(parameter):
print(parameter)
>>> myfunction("hello")
in decorate function, decorating myfunction
Executing myfunction
hello
通过使用@decorate
,myfunction
就被装饰了起来。被包装的函数将会在装饰器函数执行完毕后调用。
装饰器可将一个函数封装到另一个函数中,这样就可以方便地实现很多目标。例如,在Django之类的Web框架中,装饰器用于确保用户在执行函数之前已经处于登录状态了。
模块和作用域
模块(module)用于组织较大的Python项目,Python标准库被拆分为多个模块以便管理。大多数标准的Python函数并没有内置于语言内核中,而是通过特定的模块提供的,可以按需加载。
模块是一个包含代码的文件,其中定义了一组Python函数或其他对象,并且模块的名称来自文件名。
模块通常包含Python源代码,但也可以是经过编译的C或C++对象文件。
模块不仅可以将相互关联的Python对象归并成组,还有助于避免命名冲突(name-clash)问题。因为Python采用了命名空间(namespace)的机制,每个模块都有自己的命名空间,所以在不同模块内可以同时保留多个具有相同名称的函数。命名空间本质上就是标识符的字典,可用于代码块、函数、类、模块等。
创建第一个模块
1
2
3
4
5
6
"""mymath - our example math module"""
pi = 3.14159
def area(r):
"""area(r): return the area of a circle with radius r."""
global pi
return(pi * r * r)
使用模块时需要先导入该模块。在导入模块时,可以用from module import *
导入几乎所有的对象名称。但是,模块中下划线开头的标识符不能用from module import *
导入。此外,为了避免出现函数名覆盖的情况,原则上不应该使用这种导入方法。
1
2
3
4
5
6
7
8
9
10
import mymath
>>> mymath.pi
3.14159
>>> mymath.area(2)
12.56636
>>> mymath.__doc__
'mymath - our example math module' # 和函数一样,可以选择在模块的第一行放入文档字符串
如果在代码运行过程中修改了磁盘中的模块文件,那么再次输入import
命令并不会重新加载模块,这时要用到importlib
库的reload
函数。importlib
库为访问模块导入的后台机制提供了一个接口:
1
2
3
>>> import mymath, importlib
>>> importlib.reload(mymath)
<module 'mymath' from '/home/doc/quickpythonbook/code/mymath.py'>
当模块被第一次导入或重新加载时,模块中所有的代码都会被解析一遍。如果发现错误,则会引发语法异常。反之,如果一切正常就会创建包含Python字节码的.pyc文件,如mymath.pyc。
模块搜索路径
所有模块的搜索路径是在一个名为path
的变量中定义的,可以通过模块sys
访问path
变量。
1
2
3
4
5
6
7
8
9
10
11
12
>>> import sys
>>> sys.path
['',
'C:\\Users\\Flora\\AppData\\Local\\miniconda3\\envs\\data\\python311.zip',
'C:\\Users\\Flora\\AppData\\Local\\miniconda3\\envs\\data\\DLLs',
'C:\\Users\\Flora\\AppData\\Local\\miniconda3\\envs\\data\\Lib',
'C:\\Users\\Flora\\AppData\\Local\\miniconda3\\envs\\data',
'C:\\Users\\Flora\\AppData\\Local\\miniconda3\\envs\\data\\Lib\\site-packages',
'C:\\Users\\Flora\\AppData\\Local\\miniconda3\\envs\\data\\Lib\\site-packages\\win32',
'C:\\Users\\Flora\\AppData\\Local\\miniconda3\\envs\\data\\Lib\\site-packages\\win32\\lib',
'C:\\Users\\Flora\\AppData\\Local\\miniconda3\\envs\\data\\Lib\\site-packages\\Pythonwin'
]
当准备执行import
语句时,Python将按顺序遍历sys.path
目录列表,并采用第一个满足import
需求的模块。如果搜索路径中找不到合适的模块,则会引发ImportError异常。
sys.path
的初始值,来自操作系统环境变量PYTHONPATH
(如果存在)的值,或者来自安装时的默认值。
此外,无论何时运行Python脚本,都会把脚本文件所在目录插入sys.path
变量中,作为第一个元素,即导入模块时会首先在当前的文件夹进行搜索。通过调用sys.path[0]
确定当前执行的Python程序所在的路径。
在生产环境中,既不会以交互模式运行Python,Python代码文件也不会位于当前目录中,因此,为确保程序可以使用自己编写的模块,可以采用下面3种方式:
- 将自己的模块放入Python的常规模块搜索路径中去:最简单,但是不推荐使用,会影响Python的安装目录。
- 将Python程序要用到的全部模块,都和程序放在同一目录中:如果模块与特定的程序关联,那么这种方式是很好的选择。只要把模块和程序放在一起就可以了。
- 新建目录用于保存自己的模块,并修改
sys.path
变量,使之包含该新建目录:如果模块专用于某环境,将被同一部署环境下的多个程序调用,那么这种方式就是正确的选择。
修改sys.path
的方式也有3种:
- 可以在代码中赋值,很简单,但也会把目录位置写死(hardcode)在程序代码中。
- 可以设置环境变量
PYTHONPATH
,相对来说还算简单,但可能无法适用于当前环境的所有用户。 - 可以利用
.pth
文件将目录追加到默认搜索路径中。
Python作用域和命名空间
Python中的命名空间是从标识符到对象的映射,也就是Python如何跟踪变量和标识符是否活动以及指向什么。例如,像x = 1
这样的语句,会把x
添加到命名空间并将其与值1
关联。当在Python中执行一个代码块时,它拥有3个命名空间:局部(local)、全局(global)和内置(built-in)。
在代码运行期间遇到标识符时,Python首先会在局部命名空间中查找。如果没有找到,则接下来查看全局命名空间。如果仍未找到,则检查内置命名空间。如果标识符还不存在,将会被认为是错误,并引发NameError。
使用两个内置函数:locals
和globals
可以查看局部和全局命名空间中的绑定关系。
1
2
3
4
5
6
7
8
>>> locals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'sys': <module 'sys' (built-in)>}
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'sys': <module 'sys' (built-in)>}
>>> x = 100
>>> import math
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'sys': <module 'sys' (built-in)>, 'x': 100, 'math': <module 'math' (built-in)>}
在Shell的交互式会话中,局部和全局命名空间是相同的。创建变量并导入模块,都将会在命名空间中创建新的绑定关系。
使用dir(__builtins__)
可以打印出内置命名空间中所有的对象名称:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
'BaseExceptionGroup', 'BlockingIOError', 'BrokenPipeError', 'BufferError',
'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError',
'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError',
'Ellipsis', 'EncodingWarning', 'EnvironmentError', 'Exception', 'ExceptionGroup',
'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning',
'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError',
'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt',
'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None',
'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError',
'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError',
'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning',
'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError',
'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError',
'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError',
'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError',
'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__',
'__loader__', '__name__', '__package__', '__spec__', 'abs', 'aiter', 'all', 'anext',
'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable',
'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict',
'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format',
'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input',
'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map',
'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print',
'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
以Error
和Exit
结尾的条目是Python内置的异常名称,从abs
到zip
是Python的内置函数。
命令行环境
确保当前目录是脚本文件所在目录,然后在命令行中输入以下命令启动脚本:
1
2
$ python script1.py
this is our first test script file
Python 中可以使用 sys 的sys.argv
来获取命令行参数:
sys.argv
是命令行参数列表,sys.argv[0] 表示脚本名。len(sys.argv)
是命令行参数个数。
1
2
3
4
5
import sys
def main():
print("this is our second test script file")
print(sys.argv)
main()
1
2
3
$ python script2.py arg1 arg2 3
this is our second test script file
['script2.py', 'arg1', 'arg2', '3']
程序发布
Python脚本和应用程序的发布方式可以有很多种。
- 源代码直接发布:最简单,但是有代码被篡改的风险。
- 使用
wheel
发布:wheel
旨在让Python代码的安装更加可靠,并能帮助管理代码依赖的包。 zip
文件发布:当应用程序分散在多个模块中,可以发布为可执行的zip
文件。使用标准库中的zipapp
模块,可以从命令行或用库API创建zip app。- 单个可执行文件
exe
发布:能够在没有安装Python环境的机器上运行,可以使用Pyinstaller
、py2exe
(Windows平台)或py2app
(macOS平台)。
pyinstaller打包
安装:pip install pyinstaller
编程:新建一个名为demo
的文件夹,并在这个文件夹中新建一个名为hello.py
文件作为示例。
1
2
3
import os
print('Hello World')
os.system('pause') # 暂停程序,方便看清输出内容
打包:在demo
文件夹路径下的命令提示符窗口中输入pyinstaller hello.py
单文件打包模式 -F
PyInstaller有多文件打包和单文件打包两种模式。前者打包程序后输出多个文件并将其放入一个文件夹中,后者只输出一个可执行文件。单文件打包是在多文件打包的基础上进行的。也就是说,PyInstaller先以多文件打包模式打包程序,再将hello文件夹中的依赖文件编译到hello.exe可执行文件中。
Windows系统中在文件夹路径栏中输入
cmd
+回车键就可以快速打开当前目录下的命令提示符窗口
去除命令提示符黑框 -w
当我们运行可执行文件后,命令提示符窗口中会显示程序的输出内容。但是如果可执行文件运行失败,那么命令提示符窗口中就会显示报错信息。双击可执行文件打开后如果报错,命令提示符窗口只是一闪而过,很难捕捉报错信息,这时就需要打开一个新的命令提示符窗口,然后将hello.exe
拖入,按回车键运行即可看到报错信息。
程序要准备发布时,肯定是不希望有黑框存在的。因此在确保程序运行无误后,可以在打包命令中加上-w
来去掉黑框。
给可执行文件加上图标 -i
如果要给可执行文件添加图标,我们可以在打包时加上-i
命令,并在后面加上图标文件的路径。
pyinstaller -i /path/to/xxx.ico hello.py
用单文件打包模式打包时,图标之类的资源文件的路径无法被直接识别,需要加上--add-data
命令:
pyinstaller -F --add-data=./icon.ico;. hello.py
icon.ico
在当前路径下,所以--add-data=
后的源路径处填写./icon.ico
的话就可以让PyIsntaller找到它。之后要在打包后将icon.ico
放在hello文件夹中,所以在目标路径处填写.
为了让程序能够找到_MEI文件夹中的icon.ico
,用单文件打包模式打包前,需要修改程序代码,将res_path()函数套在各个路径上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import tkinter
import sys
import os
def res_path(relative_path):
"""获取资源路径"""
try:
# 获取_MEI文件夹所在路径
base_path = sys._MEIPASS
except Exception:
# 没有_MEI文件夹的话使用当前路径
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
win = tkinter.Tk()
win.iconbitmap(res_path('./icon.ico')) # 设置窗口图标
win.mainloop()
减少打包后文件大小
PyInstaller会把Python环境和程序使用到的库打包进来,有时候还会打包一些没有用到的第三方库,导致打包所花的时间越来越多,包也越来越大。
因此,第一个减小包体的方法就是使用干净的打包环境。所谓干净,指的是计算机上只安装了程序运行所必需的库。通常,我们会使用虚拟环境,或者在虚拟机中打包。
第二个方法是使用--exclude-module
命令指定不需要打包的库。
pyinstaller --exclude-module=numpy --exclude-module=pandas hello.py
第三个方法是使用UPX工具,它可以进一步压缩可执行文件。
pyinstaller工作流
- 确定打包命令。只有一个.py文件,图片放在images文件夹中,使用upx进行压缩,程序需要图标。
1
pyinstaller --add-data=./images/*;./images --upx-dir=./upx-3.96-win64 -i ./icon.ico hello.py
- 开始打包。在hello.py所在文件夹路径的命令提示符窗口中输入确定好的打包命令。
- 解决报错并重新打包。运行可执行文件,如果出现报错,则根据报错内容修改代码或打包命令后,重新打包。
- 查看是否有多余文件。打开dist文件夹中的hello文件夹,如果发现里面多了一些在程序中没有引入的库或模块,则需要在打包命令中加上“–exclude-module”命令删除它们。
- 去掉黑框。当以上几个步骤都没有问题之后,再加上“-w”命令去掉黑框。
nuitka打包
Nuitka会把要打包的代码编译成C语言版本,这样不仅可提升程序的运行效率,也可加强代码安全性。这个打包库使用起来也很简单,而且有很多打包逻辑跟PyInstaller的打包逻辑是类似的,比如Nuitka也有单文件打包模式和多文件打包模式,也需要通过黑框来查看报错信息。
nuitka安装
- 安装:
pip sintall nuitka
- 配置MinGW-w64和GCC:在Windows系统,Nuitka需要使用MinGW-w64和GCC来将代码编译成可执行文件。将下载下来的压缩包解压到任意位置,单击进入mingw64文件夹中的bin文件夹,将该路径添加到计算机环境变量中,最后我们在命令提示符窗口中执行
gcc --version
命令,如果出现GCC的版本号,则表明配置成功。 - 配置ccache:ccache用来缓存编译时生成的信息,可以让Nuitka在下一次编译时利用缓存加快编译的速度。将下载下来的压缩包解压到任意位置,单击进入解压出来的文件夹,并将该路径添加到环境变量中。最后我们在命令提示符窗口中执行
ccache --version
命令,如果出现ccache的版本号,则表明配置成功。 - 配置Dependency Walker:Nuitka会使用Dependency Walker来获取Python扩展模块的依赖文件,会在使用
--standalone
和--onefile
命令时用到。将下载下来的压缩包解压到任意位置,单击进入解压出来的文件夹(其中有depends.exe),将depends.exe的路径配置到环境变量中。
常用命令
- 打包模式:
nuitka --standalone hello.py
和nuitka --onefile hello.py
- 添加图标:
--windows-icon-from-ico=/path/to/xxx.ico
- 从其他exe文件提取图标:
--windows-icon-from-exe=/path/to/exe
- 添加单个资源文件:
--include-data-file=资源文件源路径=资源文件相对于可执行文件的路径
- 添加资源文件夹:
--include-data-dir=资源文件夹源路径=资源文件夹相对于可执行文件的路径
- 查看Nuitka添加了插件支持的第三方库:
nuitka --plugin-list
- 使用第三方库的插件打包:
--enable-plugin=pyqt5
在用Nuitka打包一些大型第三方库时,如果发现使用了“–enable-plugin”命令后打包速度非常慢,我们就可以考虑不使用插件,而是在打包结束后直接复制库的相关文件,或者通过“–include-data-dir”命令打包库文件夹。
以打包numpy程序为例,首先从Python安装目录下找到numpy文件夹,然后将其复制到demo文件夹中,使用以下命令打包:
nuitka --standalone --nofollow-imports --include-data-dir=./numpy=./numpy hello.py
“–nofollow-imports”命令可以让Nuitka不去自动分析程序中引入的库,以加快打包速度。“–include-data-dir”命令用来打包numpy文件夹。
nuitka工作流
- 确定打包命令
我们决定用多文件打包模式打包,需要添加“–standalone”命令。hello.py是入口文件。另外,该程序使用了一些图标文件,且这些文件全部都存放在images文件夹下,所以我们还要用到“–include-data-dir”命令。要给可执行文件加上图标,还需要使用“–windows-icon-from-ico”命令。程序因为使用了PyQt5,所以我们可以通过“–enable-plugin”命令启用PyQt5插件。
nuitka –standalone --include-data-dir=./images=./images --enable-plugin=pyqt5 --windows-icon-from-ico=./icon.ico hello.py
- 开始打包
- 解决报错并重新打包
- 去掉黑框
nuitka --standalone --include-data-dir=./images=./images --enable-plugin=pyqt5 --windows-icon-from-ico=./icon.ico --windows-disable-console hello.py
- 减小包体
使用UPX工具压缩生成的hello.exe,命令为“upx -9 hello.exe”。如果用的是“–onefile”命令,那么Nuitka会在打包时自动使用zstandard模块(需要使用pip安装)压缩生成的可执行文件。
PYPI发布
PyPI 是 Python Package Index 的首字母简写,其实表示的是 Python 的 Package 索引,这个也是 Python 的官方索引,官方地址:https://pypi.org/。需要先在本地环境安装pip
,如果要安装其他工具包的话就使用指令:
1
pip install <package name>
实战教程:如何将自己的Python包发布到PyPI上-阿里云开发者社区
文件系统
文件路径
文件有两个关键属性:“文件名”(filename,通常为一个单词)和“路径”。
根目录root和主目录home
在Windows操作系统中,根文件夹名为C:\
,也称为C盘。在macOS和Linux操作系统中,根文件夹是/
。
所有用户在计算机上都有一个用于存放自己文件的文件夹,该文件夹称为“主目录”或“主文件夹”,不同操作系统的主目录也会有所区别。利用Path.home()
可以获得主路径的字符串。
Windows | macOS | Linux |
---|---|---|
C:\Users | /Users | /home |
文件夹名称和文件名在Windows和macOS上是不区分大小写的,但在Linux上是区分大小写的。
绝对路径与相对路径
绝对路径总是从根文件夹开始。相对路径则是相对于程序的当前工作目录。
相对路径还有.
和..
文件夹。它们不是真正的文件夹,而是可以在路径中使用的特殊名称。
正斜杠和反斜杠
用pathlib模块的Path()
函数处理Windows上的倒斜杠\
以及macOS和Linux上的正斜杠/
如果将单个文件和路径上的文件夹名称的字符串传递给它,Path()就会返回一个适合当前操作系统的文件路径字符串,包含正确的路径分隔符。
1
2
3
4
5
>>> from pathlib import Path
>>> Path('spam','bacon','eggs')
WindowsPath('spam/bacon/eggs')
>>> str(Path('spam','bacon','eggs'))
'spam\\bacon\\eggs'
由于每个倒斜杠都需要用另一个倒斜杠字符进行转义,因此倒斜杠会加倍。一般情况下,为了避免混淆,尽量使用符合Linux标准的正斜杠分割目录
将/
运算符与Path对象一起使用,连接路径就像连接字符串一样容易。
1
2
3
4
5
>>> from pathlib import Path
>>> Path('spam') /'bacon' / 'eggs'
WindowsPath('spam/bacon/eggs')
>>> Path('spam') / Path('bacon','eggs')
WindowsPath('spam/bacon/eggs')
获取当前目录
利用Path.cwd()
函数,可以取得当前工作路径的字符串,并可以利用os.chdir()
改变它。
1
2
3
4
5
6
7
>>> from pathlib import Path
>>> import os
>>> Path.cwd()
WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37')'
>>> os.chdir('C:\\Windows\\System32')
>>> Path.cwd()
WindowsPath('C:/Windows/System32')
文件操作
创建文件夹
可以通过绝对路径或通过Path对象创建文件夹。注意,mkdir()一次只能创建一个目录。它不会像os.makedirs()一样同时创建多个子目录。可以使用os.path.exists(path)
判断文件是否已经存在
1
2
3
4
5
>>> import os
>>> os.makedirs('C:\\delicious\\walnut\\waffles')
>> from pathlib import Path
>>> Path(r'C:\Users\Al\spam').mkdir()
复制文件和文件夹
shutil(shell util)模块中包含一些函数,让你可以在Python程序中复制、移动、重命名和删除文件。
调用shutil.copy(source, destination)
,将路径source处的文件复制到路径destination处的文件夹(source和destination都是字符串)。如果destination是一个文件名,那么它将作为被复制文件的新名字。该函数返回一个字符串,表示被复制文件的路径。
1
2
3
4
5
6
7
>>> import shutil, os
>>> from pathlib import Path
>>> p = Path.home()
>>> shutil.copy(p/'spam.txt', p / 'some_folder')
'C:\\Users\\Al\\some_folder\\spam.txt'
>>> shutil.copy(p / 'eggs.txt', p / 'some_folder/eggs2.txt')
WindowsPath('C:/Users/Al/some_folder/eggs2.txt')
shutil.copy()
将复制一个文件,shutil.copytree()
将复制整个文件夹以及它包含的文件夹和文件。调用shutil.copytree(source,destination)
,将路径source处的文件夹(包括它的所有文件和子文件夹)复制到路径destination处的文件夹。source和destination参数都是字符串。该函数返回一个字符串,该字符串是新复制的文件夹的路径。
1
2
3
4
5
>>> import shutil, os
>>> from pathlib import Path
>>> p = Path.home()
>>> shutil.copytree(p / 'spam', p / 'spam_backup')
WindowsPath('C:/Users/Al/spam_backup') # 常用于备份文件夹
移动与重命名文件和文件夹
调用shutil.move(source, destination)
,将路径source处的文件夹移动到路径destination,并返回新位置的绝对路径的字符串。如果destination指向一个文件夹,那么source文件将移动到destination中,并保持原来的文件名。例如,在交互式环境中输入以下代码:
1
2
3
>>> import shutil
>>> shutil.move('C:\\bacon.txt', 'C:\\eggs')
'C:\\eggs\\bacon.txt'
如果在C:\eggs中已经存在一个文件bacon.txt,那么它就会被覆盖。因为用这种方式很容易不小心覆盖文件,所以在使用时应该注意。
destination路径也可以指定一个文件名,这样source文件就会被移动并重命名。如果没有eggs文件夹,那么move()就会将bacon.txt重命名,变成名为eggs的文件:
>>> shutil.move('C:\\bacon.txt', 'C:\\eggs')
这里,move()在C:\目录下找不到名为eggs的文件夹,因此假定destination指的是一个文件,而不是文件夹。bacon.txt文本文件会被重命名为eggs(没有.txt文件扩展名的文本文件),但这可能不是你所希望的。
删除文件和文件夹
利用os模块,可以删除一个文件或一个空文件夹。利用shutil模块,可以删除一个文件夹及其所有的内容。
- 调用
os.unlink(path)
将删除path处的文件。 - 调用
os.rmdir(path)
将删除path处的文件夹。该文件夹必须为空,其中不能有任何文件和文件夹。 - 调用
shutil.rmtree(path)
将删除path处的文件夹,它包含的所有文件和文件夹都会被删除。
在程序中使用这些函数时要小心。可以在第一次运行程序时注释掉这些调用,并且加上print()
,显示会被删除的文件。这样做是一个好方法。
下面有一个Python程序,本来打算删除具有.txt扩展名的文件,但有一处录入错误(用粗体突出显示),结果导致它删除了.rxt文件:
1
2
3
4
import os
from pathlib import Path
for filename in Path.home().glob('*.rxt'):
os.unlink(filename)
如果你有某些文件以.rxt结尾,它们就会被永久地删除。作为替代,你应该先运行像这样的程序:
1
2
3
4
5
import os
from pathlib import Path
for filename in Path.home().glob('*.rxt'):
# os.unlink(filename)
print(filename)
先运行这个版本的程序,你就会知道,你不小心告诉程序要删除.rxt文件,而不是.txt文件。在确定程序按照你的意图工作后,删除print(filename)
代码行,取消os.unlink(filename)
代码行的注释。然后再次运行该程序,实际删除这些文件。
安全删除模块 send2trash
因为Python内置的shutil.rmtree()函数将不可恢复地删除文件和文件夹,所以用起来可能有危险。删除文件和文件夹更好的方法是使用第三方的send2trash模块。
1
2
3
4
5
6
>>> import send2trash
>>> baconFile = open('bacon.txt', 'a') # creates the file
>>> baconFile.write('Bacon is not a vegetable.')
25
>>> baconFile.close()
>>> send2trash.send2trash('bacon.txt')
一般来说,总是应该使用send2trash.send2trash()
函数来删除文件和文件夹。虽然它将文件发送到回收站,让你稍后能够恢复它们,但是这不像永久删除文件,它不会释放磁盘空间。如果你希望程序释放磁盘空间,就要用os和shutil来删除文件和文件夹。send2trash()
函数只能将文件发送到回收站,不能从中恢复文件。
遍历目录树 os.walk()
假定你希望对某个文件夹中的所有文件进行重命名,包括该文件夹中所有子文件夹中的所有文件。也就是说,你希望遍历目录树,并处理遇到的每个文件。
1
2
3
4
5
6
7
8
import os
for folderName, subfolders, filenames in os.walk('C:\\delicious'):
print('The current folder is ' + folderName)
for subfolder in subfolders:
print('SUBFOLDER OF ' + folderName + ': ' + subfolder)
for filename in filenames:
print('FILE INSIDE ' + folderName + ': '+ filename)
print('')
os.walk()
函数被传入一个字符串值,即一个文件夹的路径。你可以在一个for循环语句中使用os.walk()
函数遍历目录树,os.walk()
在循环的每次迭代中返回以下3个值。
- 当前文件夹名称的字符串。
- 当前文件夹中子文件夹的字符串的列表。
- 当前文件夹中文件的字符串的列表。
所谓当前文件夹,是指for循环时迭代的文件夹。程序的当前工作目录不会因为os.walk()而改变。
纯文本读写
纯文本文件和二进制文件
纯文本文件只包含基本文本字符,不包含字体、大小和颜色信息。带有.txt扩展名的文本文件,以及带有.py扩展名的Python脚本文件,都是纯文本文件的例子。
二进制文件包含所有其他文件类型,如PDF、图像、电子表格和可执行程序。如果用Notepad或TextEdit打开一个二进制文件,它看起来就像乱码,因此每种不同类型的二进制文件都必须用它自己的方式来处理。
创建和读取纯文本
pathlib模块的read_text()
方法返回文本文件全部内容的字符串。它的write_text()
方法利用传递给它的字符串创建一个新的文本文件(或覆盖现有文件)。
1
2
3
4
5
6
>>> from pathlib import Path
>>> p = Path('spam.txt')
>>> p.write_text('Hello, world!')
13
>>> p.read_text()
'Hello, world!'
读写文件
1.调用open()
函数,返回一个File对象。
2.调用File对象的read()
或write()
方法。
3.调用File对象的close()
方法,关闭该文件。
打开或创建文件:open()
传递的参数既可以是绝对路径,也可以是相对路径。open()
函数返回一个File对象。
如果传递给open()
的文件名不存在,则写模式和添加模式都会创建一个新的空文件。
1
2
>>> helloFile = open(Path.home() / 'hello.txt')
>>> helloFile = open('C:\\Users\\your_home_folder\\hello.txt')
Python默认将以只读模式打开文件,也可以通过open()
的第二个参数来手动指定:
r
:读模式
w
:写模式
a
:添加模式
读取文件内容:read()
有了一个File对象,就可以开始从它里面读取内容。
如果你希望将整个文件的内容读取为一个字符串,就使用File对象的read()方法。
1
2
3
>>> helloContent = helloFile.read()
>>> helloContent
'Hello, world!'
也可以使用readlines()
方法,从该文件取得一个字符串的列表。列表中的每个字符串就是文本中的每一行。
写入文件:write()
在读取或写入文件后,调用close()
方法,然后才能再次打开该文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> baconFile = open('bacon.txt', 'w')
>>> baconFile.write('Hello, world!\n')
13
>>> baconFile.close()
>>> baconFile = open('bacon.txt', 'a')
>>> baconFile.write('Bacon is not a vegetable.')
25
>>> baconFile.close()
>>> baconFile = open('bacon.txt')
>>> content = baconFile.read()
>>> baconFile.close()
>>> print(content)
Hello, world!
Bacon is not a vegetable.
write()
函数会返回写入的字符个数,包括换行符。write()
不会像print()
那样在字符串的末尾自动添加换行符,必须手动添加换行符。
用with open() as file:
打开文件,操作完成后自动关闭,无需再手动输入close()
函数
用shelve模块保存数据
利用shelve模块,你可以将Python程序中的变量保存到二进制的shelf文件中。这样,程序就可以从硬盘中恢复变量的数据了。shelve模块让你在程序中添加“保存”和“打开”功能。例如,如果运行一个程序,并输入了一些设置,就可以将这些设置保存到一个shelf文件中,然后让程序下一次运行时加载它们。
1
2
3
4
5
>>> import shelve
>>> shelfFile = shelve.open('mydata')
>>> cats = ['Zophie', 'Pooka', 'Simon']
>>> shelfFile['cats'] = cats
>>> shelfFile.close()
在Windows操作系统上运行前面的代码,你会看到在当前工作目录下有3个新文件:mydata.bak、mydata.dat和mydata.dir。在macOS上,只有mydata.db文件会被创建。这些二进制文件包含了存储在shelf中的数据。
你的程序稍后可以使用shelve模块重新打开这些文件并取出数据。shelf值不必用读模式或写模式打开,因为它们在打开后既能读又能写。
1
2
3
4
5
6
>>> shelfFile = shelve.open('mydata')
>>> type(shelfFile)
<class 'shelve.DbfilenameShelf'>
>>> shelfFile['cats']
['Zophie', 'Pooka', 'Simon']
>>> shelfFile.close()
就像字典一样,shelf值有keys()和values()方法,它们返回shelf中键和值的类似列表的值。因为这些方法返回类似列表的值,而不是真正的列表,所以应该将它们传递给list()函数来取得列表的形式。
1
2
3
4
5
6
>>> shelfFile = shelve.open('mydata')
>>> list(shelfFile.keys())
['cats']
>>> list(shelfFile.values())
[['Zophie', 'Pooka', 'Simon']]
>>> shelfFile.close()
异常处理
产生异常的动作被称为引发(raise)或抛出(throw)异常。
响应异常的动作被称为捕获(catch)异常,处理异常的代码则称为异常处理代码(exception-handling code)或简称为异常处理程序(exception handler)。
根据引发异常的事件不同,程序可能需要采取不同的操作。磁盘空间耗尽、内存不足、除零错误,引发这些异常时的处理方式都完全不同。处理多种异常的方案之一,就是全局记录一条标识异常原因的错误消息,并让所有的异常处理程序都检查该错误消息并进行适当的操作。
与大多数实现了异常机制的现代语言一样,Python并不是只定义了一种异常,而是定义多种不同类型的异常,对应于可能发生的各种问题。根据底层事件的不同,可以引发不同类型的异常。此外,还可以让捕获异常的代码仅捕获特定类型的异常。
异常是Python函数用raise
语句自动生成的对象。在异常对象生成后,引发异常的raise
语句将改变Python程序的执行方式,这与正常的执行流程不同了。不是继续执行raise
的下一条语句,也不执行生成异常后的下一条语句,而是检索当前函数调用链,查找能够处理当前异常的处理程序。如果找到了异常处理程序,则会调用它,并访问异常对象获取更多信息。如果找不到合适的异常处理程序,程序将会中止并报错。
Python异常的类型按照大类可分为SystemExit、KeyboardInterrupt、GeneratorExit、Exception。Exception类下面还有更详细的分类,每种异常都是一种Python类,继承自父异常类。由于大部分异常都继承自Exception,强烈建议所有的用户自定义异常也都应是Exception的子类。
异常机制的重点,并不是要让程序带着错误消息中止运行。要在程序中实现中止功能不是什么难事。异常机制的特别之处在于,不一定会让程序停止运行。通过定义合适的异常处理代码,就可以保证常见的异常情况不会让程序运行失败。或许可以通过向用户显示错误消息或其他方法,或许还可能把问题解决掉,但是不会让程序崩溃。
自定义新的异常
下面的代码创建了一个类,该类将继承基类Exception中的所有内容。
1
2
class MyError(Exception):
pass
异常可以像其他任何异常一样引发、捕获和处理。如果给出一个参数,并且未经捕获和处理,参数值就会在跟踪信息的最后被打印出来:
1
2
3
4
>>> raise MyError("Some information about what went wrong")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
__main__.MyError: Some information about what went wrong
当然,上述参数在自己编写的异常处理代码中也是可以访问到的:
1
2
3
4
5
6
7
try:
raise MyError("Some information about what went wrong")
except MyError as error:
print("Situation:", error)
# 运行结果
Situation: Some information about what went wrong
try语句
- try关键字:用于检测异常,在程序发生异常时将异常信息交给except关键字。
- except关键字:获取异常并进行处理。
- else关键字:当执行完try关键字域中的代码,如果没有发现异常,则接着执行else关键字域中的代码。
- finally关键字:无论是否发生异常都进入该关键字域进行处理,通常用于处理资源关闭、对象内存释放等必需的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
try:
<语句> # 待捕获异常的代码
except <异常类>:
<语句> # 捕获某种类型的异常
except <异常类> as <变量名>:
<语句> # 捕获某种类型的异常并获得对象
else:
<语句> # 如果没有异常发生,则执行
finally:
<语句> # 退出try时总会执行,不管是否发生了异常,都要执行finally的部分
抛出异常 raise
抛出异常使用raise语句。在代码中,raise语句包含以下部分。
- raise关键字。
- 对Exception()函数的调用。
- 传递给Exception()函数的字符串,包含有用的错误信息。
例如,在交互式环境中输入以下代码:
1
2
3
4
5
>>> raise Exception('This is the error message.')
Traceback (most recent call last):
File "<pyshell#191>", line 1, in <module>
raise Exception('This is the error message.')
Exception: This is the error message.
如果没有try和except语句来覆盖抛出异常的raise语句,那么该程序就会崩溃,并显示异常的错误信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def boxPrint(symbol, width, height):
if len(symbol) != 1:
raise Exception('Symbol must be a single character string.')
if width <= 2:
raise Exception('Width must be greater than 2.')
if height <= 2:
raise Exception('Height must be greater than 2.')
print(symbol * width)
for i in range(height - 2):
print(symbol + (' ' * (width - 2)) + symbol)
print(symbol * width)
for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
try:
boxPrint(sym, w, h)
except Exception as err:
print('An exception happened: ' + str(err))
这里定义了一个boxPrint()
函数,它接收一个字符、一个宽度值和一个高度值,按照指定的宽度和高度,用该字符创建了一个小盒子的图像并被输出到屏幕上。假定我们希望该字符是一个字符,且宽度和高度要大于2。我们添加了if语句,如果这些条件没有满足,就抛出异常。
稍后,当我们用不同的参数调用boxPrint()
时,try…except
语句就会处理无效的参数。
回溯 traceback库
在日常开发中,通常会做一些基本的异常处理,但是有时候异常处理只能打印出处理的结果或者简单的异常信息,并不能直观的知道在哪个文件中的哪一行出错。这时候可以使用traceback库。
最常见用法是:在主函数的except
语句内,导入traceback库并使用print_exc()
函数打印出详细的异常信息,包括引发异常的代码文件以及行数,便于排查问题。
但是在最终交付的时候,为了避免程序代码泄露,最好不要使用traceback库来显示异常。#
如果Python遇到错误,它就会生成一些错误信息,称为“回溯”。回溯包含了错误信息、导致该错误的代码行号,以及导致该错误的函数调用的序列。这个序列称为“调用栈”。
1
2
3
4
5
def spam():
bacon()
def bacon():
raise Exception('This is the error message.')
spam()
如果运行errorExample.py,输出结果看起来像这样:
1
2
3
4
5
6
7
8
Traceback (most recent call last):
File "errorExample.py", line 7, in <module>
spam()
File "errorExample.py", line 2, in spam
bacon()
File "errorExample.py", line 5, in bacon
raise Exception('This is the error message.')
Exception: This is the error message.
根据回溯,可以看到该错误发生在第5行,在bacon()函数中。这次特定的bacon()调用来自第2行,在spam()函数中,它又在第7行被调用。在可能从多个位置调用函数的程序中,调用栈能帮助你确定哪次调用导致了错误。
只要抛出的异常没有被处理,Python 就会显示回溯。你也可以调用traceback.format_ exc()得到它的字符串形式。如果你希望得到异常的回溯的信息,也希望except语句能优雅地处理该异常,那么使用这个函数就很有用。在调用该函数之前,需要导入Python的traceback模块。
例如,不是让程序在异常发生时就崩溃,而是将回溯信息写入一个日志文件,并让程序继续运行。稍后,在准备调试程序时,我们可以检查该日志文件。
1
2
3
4
5
6
7
8
9
10
>>> import traceback
>>> try:
... raise Exception('This is the error message.')
except:
... errorFile = open('errorInfo.txt', 'w')
... errorFile.write(traceback.format_exc())
... errorFile.close()
... print('The traceback info was written to errorInfo.txt.')
111
The traceback info was written to errorInfo.txt.
write()方法的返回值是111,因为有111个字符被写入文件中。回溯文本被写入errorInfo.txt。
断言 assert
断言是健全性检查,用于确保代码没有做什么明显错误的事情。这些健全性检查由assert语句执行。如果检查失败,就会抛出异常。在代码中,assert语句包含以下部分:
1
assert expression, argument
如果expression的结算结果为False
,同时系统变量__debug__为True
,则会引发携带可选参数argument的AssertionError
异常。可选参数argument可用于放置对该assert
的解释信息。
assert语句表达的是:我断言条件成立,如果条件不成立,则说明某个地方有bug,应立即停止程序。
1
2
3
4
5
>>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73]
>>> ages.sort()
>>> ages
[15, 17, 22, 26, 47, 54, 57, 73, 80, 92]
>>> assert ages[0] <= ages[-1]
这里的assert语句断言ages中的第一项应小于或等于最后一项。这是健全性检查;如果sort()中的代码没有错误,并且可以完成工作,则该断言为真。
假设我们的代码中有一个错误,不小心调用了reverse()列表方法,而不是sort()列表方法。在交互式环境中输入以下内容时,assert语句将引发AssertionError:
1
2
3
4
5
6
7
8
>>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73]
>>> ages.reverse()
>>> ages
[73, 47, 80, 17, 15, 22, 54, 92, 57, 26]
>>> assert ages[0] <= ages[-1]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
不像异常,代码不应该用try和except处理assert语句。如果assert失败,那么程序就应该崩溃。通过这样的快速失败,产生bug和第一次注意到该bug之间的时间就缩短了。这将减少为了寻找bug的原因而需要检查的代码量。
断言针对的是程序员的错误,而不是用户的错误。
断言只能在程序正在开发时失败,用户永远都不会在完成的程序中看到断言错误。因此,断言常用于pytest测试中。
对于程序在正常运行中可能遇到的那些错误(如文件没有找到,或用户输入了无效的数据),请抛出异常,而不是用assert语句检测它。不应使用assert语句来引发异常,因为用户可以选择关闭断言。
如果你使用python -O myscript.py
而不是python myscript.py
运行Python脚本,那么Python将跳过assert语句。
日志 logging库
Python的logging模块使你很容易创建自定义的消息记录。这些日志消息将描述程序何时调用日志函数,并列出你指定的任何变量当时的值。另一方面,缺失日志消息表明有一部分代码被跳过,从未执行。
要启用logging模块以在程序运行时将日志消息显示在屏幕上,请将下面的代码复制到程序顶部(但在Python的#!行之下):
1
2
import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
当 Python 记录一个事件的日志时,它都会创建一个LogRecord对象以保存关于该事件的信息。logging模块的函数让你指定想看到的这个LogRecord对象的细节,以及希望的细节展示方式。
类和面向对象编程
封装、继承和多态性是面向对象编程的三大要素。
类
类由表示对象状态的数据和规定对象行为的方法(method)所构成。Python的所有内置数据类型都是类。
以一个表示某健身房会员的类为例:
1
2
class Member: # 定义类时,类名称后面不需要加()
pass
为了能让类标识符足够醒目,按惯例每个单词首字母应该大写。类定义完之后,只要将类名称作为函数进行调用,就可以创建该类的对象,即类的实例:
1
jack = Member()
通过在类的定义中包含初始化方法__init__()
,可以实现对实例的字段进行自动初始化。每次创建类的新实例时,该函数都会运行,新建实例本身将作为函数的第一个参数self
代入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Member:
def __init__(self, no: int, name: str, weight: float) -> None:
self.no = no
self.name = name
self.weight = weight
def print(self) -> None:
print(f"{self.no}: {self.name} {self.weight} kg.")
jack = Member(15, "Jack Jones", 72.7)
mike = Member(37, "Mike James", 65.3)
jack.print()
15: Jack Jones 72.7 kg.
Member类的类体定义了3个实例变量no
、name
、weight
和2个方法__init__
、print
。
方法是属于某个类的函数。与__init__
方法类似,print
方法被定义为类定义内部的函数。方法的第一个参数一定是发起调用的实例,按惯例命名为self
。
对于方法调用instance.method(arg1, arg2, ...)
,Python将按以下规则将其转换为普通的函数调用:
- 先在实例的命名空间中查找方法名。如果方法在该实例中被修改或添加过,那就会优先调用该实例中的方法,而不是类或父类中的方法。
- 如果在实例的命名空间中找不到该方法,就会找到实例的类型,也就是其所属的类,并在其中查找该方法。
- 如果方法还未找到,就查找父类(superclass)中的方法。
- 如果方法找到了,就会像普通的Python函数一样被直接调用,函数的第一个参数将是
instance
,方法调用中的其他参数则整体向右平移一个位置传入函数。因此instance.method(arg1, arg2, ...)
就会成为class.method(instance, arg1, arg2, ...)
。
数据隐藏与封装
下面我们对健身房会员的Member类的代码进行两个修改。
- 使类外部的代码不能(随意)修改会员号码、名字和体重值。
- 添加减重方法 lose_weight。
1
2
3
4
5
6
7
8
9
10
11
class Member:
def __init__(self, no: int, name: str, weight: float) -> None:
self.__no = no
self.__name = name
self.__weight = weight
def lose_weight(self, loss: float) -> None:
self.__weight -= loss
def print(self) -> None:
print(f"{self.__no}: {self.__name} {self.__weight} kg.")
实例变量的名称开头有两个下划线后,便无法从类的外部访问类的属性。像这样,类内部的数据无法(难以)从类外部访问的性质称为数据隐藏(data hiding)。
同时,lose_weight
方法接收了形参loss
后会从体重中减去相应的数值。以所属实例的数据值为基础进行处理操作或更新其数据值。这种将数据和方法紧密相连的操作称为封装(encapsulation)。
存取器和@property
在经营健身房时需要频繁获取和修改各个会员的体重值,因此我们要在类中添加用来获取和修改体重值的功能。
为实现上述功能,我们需要定义以下两个方法。
1
2
3
4
5
def get_weight(self) -> float:
return self.weight
def set_weight(self, weight: float) -> None:
self.__weight == weight
像这样,获取和修改数据属性值的方法分别称为访问器(getter)和修改器(setter),统称为存取器(accessor)。一般会使用@property
装饰器来定义访问器和修改器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Member:
def __init__(self, no: int, name: str, weight: float) -> None:
self.__no = no
self.__name = name
self.__weight = weight
def lose_weight(self, loss: float) -> None:
self.__weight -= loss
def print(self) -> None:
print(f"{self.__no}: {self.__name} {self.__weight} kg.")
@property
def weight(self) -> float:
return self.weight
@weight.setter
def weight(self, weight: float) -> None:
self.__weight == weight if weight > 0.0 else 0.0
定义访问器时要添加前置项@property
- 访问器的方法名要能表示出数据属性,该名称叫作存取器名。
- 访问器本身返回了数据的值。
定义修改器时要添加前置项@存取器名.setter
- 修改器方法的名称与访问器的名称,即存取器的名称一样。
- 修改器接收形参的值后,对数据进行了赋值操作。
定义修改器和访问器后,我们可以使用实例名.存取器名
这种形式的表达式获取和修改数据值。
1
2
3
jack = Member(15, "Jack Jones", 72.7)
jack.weight = 68
print(f"jack's latest weight: {jack.weight}")
类变量和类方法
类变量(class variable)是与类关联的变量,而不是与类的实例关联,并且可供类的所有实例访问。类变量可用于保存类级别的数据,例如,在某一时刻已创建了多少个该类的实例。
类变量是通过类定义代码中的赋值语句创建的,而实例变量是在__init__
方法中创建的。
类变量创建之后,就可被类的所有实例看到。在类的方法中访问类变量,只要带上类名即可。
1
2
3
4
5
6
7
8
9
class Circle:
pi = 3.14159
def __init__(self, radius):
self.radius = radius
def area(self):
return self.radius * self.radius * Circle.pi
Circle.pi
3.14159
如果不想在上述类的方法中把类名写死。通过特殊的__class__
属性可以避免这种写法,该属性可供Python类的所有实例访问。__class__
属性会返回实例所属的类。
1
2
3
# 重写area方法,使其代码中不出现类的具体名称
def area(self):
return self.radius * self.radius * self.__class__.pi
Python在查找实例变量时,如果找不到具有该名称的实例变量,就会在同名的类变量中查找并返回类变量值。因此,类变量可以高效地实现实例变量的默认值,只需创建一个具有合适默认值的同名类变量,就能避免每次创建类实例时初始化该实例变量的时间和内存开销。但这也很容易在无意之中造成实例变量和类变量的混用,且不会有任何报错信息。
1
2
3
4
5
6
7
>>> c1 = Circle(1)
>>> c2 = Circle(2)
>>> c1.pi = 3.14
>>> c1.pi
3.14
>>> c2.pi
3.14159
上面的代码中,通过c1.pi=3.14
在c1
中新建了一个实例变量pi
,它不会对类变量Circle.pi
产生任何影响。尽管c2
未包含名为pi
的关联实例变量,但调用c2.pi
,Python首先会寻找实例变量pi
。如果找不到实例变量,Python就会查找Circle
并找到类变量pi
。
因此,如果需要更改类变量的值,请通过类名进行访问,而不要通过实例变量self
。
在介绍类方法之前,先介绍一下静态方法。不管有没有创建类的实例,静态方法都是可以调用的,因此,静态方法不需要传入和实例相关的self
参数。静态方法常见的用途,就是实现一些和类相关的工具函数。
使用@staticmethod装饰器可以创建静态方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""circle module: contains the Circle class."""
class Circle:
all_circles = []
pi = 3.14159
def __init__(self, radius):
self.radius = radius
self.__class__.all_circles.append(self)
def area(self):
return self.radius * self.radius * self.__class__.pi
@staticmethod # 声明静态方法
def total_area(): # 静态方法可以不用self参数
total = 0
for c in self.__class__.all_circles:
total = total + c.area()
return total
类方法与静态方法很相像,都可以在类的对象被实例化之前进行调用,也都能通过类的实例来调用。但是类方法隐式地将所属类作为第一个参数进行传递,因此代码可以更简单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""circle module: contains the Circle class."""
class Circle:
all_circles = []
pi = 3.14159
def __init__(self, radius):
self.radius = radius
self.__class__.all_circles.append(self)
def area(self):
return self.radius * self.radius * self.__class__.pi
@classmethod # 声明类方法
def total_area(cls):
total = 0
for c in cls.all_circles: # 类作为参数,按惯例命名为cls
total = total + c.area() # 用cls代替self.__class__
return total
继承
因为Python的动态性,对语言没有加太多限制,所以其继承机制要比Java和C ++等编译型语言更加简单灵活。
在Python中使用继承类通常有两个要求:
- 定义继承的层次结构,在用
class
定义类名之后的圆括号中,给出要继承的类即可。 - 必须显式调用被继承类的
__init__
方法。Python不会自动执行初始化操作,但可以用super
函数让Python找到被继承的类。如下所示,初始化的工作由super().__ init __(x, y)
这行代码来完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Shape:
def __init__(self, x, y):
self.x = x
self.y = y
class Square(Shape): # 声明Square继承自Shape
def __init__(self, side=1, x=0, y=0):
super().__init__(x, y) # Shape的__init__方法必须得调用
self.side = side
class Circle(Shape): # 声明Circle继承自Shape
def __init__(self, radius=1, x=0, y=0):
super().__init__(x, y) # Shape的__init__方法必须得调用
self.radius = radius
可以不用super
来调用Shape
的__init__
,而是用Shape.__init__(self, x, y)
显式给出被继承类的名字,同样能够实现在实例初始化完毕后调用Shape
的初始化函数。从长远来看,这种做法不够灵活,因为对被继承类名进行了硬编码。如果日后整体设计和继承架构发生了变化,这就可能成为问题。
但在继承关系比较复杂的时候,采用super
会比较麻烦。因为这两种方案无法完全混合使用,所以需要把代码中采用的方案清楚地记录在文档中备查。
如果方法未在子类或派生类中定义,但在父类中有定义,继承机制也会生效。
除了方法之外,类变量和实例变量也可以进行继承:
类实例可以继承类的属性。实例变量是和对象实例关联的,某个名称的实例变量在一个实例中只会存在一个。
类变量是支持继承的,但应该避免命名冲突。
多重继承
多重继承(multiple inheritance)是指对象从多个父类继承数据和行为,编译型语言对多重继承的使用做了严格的限制。例如,在C++中,多重继承的使用规则非常复杂,很多人都敬而远之。在Java中,不允许多重继承。
Python对多重继承没有类似的限制。类可以继承自任意数量的父类,方式与从单个父类继承是一样的。最简单的情况是,所有类(包括通过父类间接继承的类)都不包含实例变量或同名的方法。在这种情况下,继承类的行为就像是自己和全部祖先类定义的整合。
如果有多个类共用相同的方法名时,情况会复杂一些,因为Python必须确定哪个名称才是要用的。答案取决于Python查找父类的顺序,如果方法在初始发起调用的类中没有定义,Python就会按照该顺序进行查找。在最简单的情况下,Python将按照从左到右的顺序查找所有基类。但在进入下一个基类之前,总是会先查看当前基类的所有祖先类。
类实例的作用域和命名空间
在类的方法中,可以直接访问局部命名空间(在方法内声明的参数和变量)、全局命名空间(在模块级别声明的函数和变量)以及内置命名空间(内置函数和内置异常)。三者将按以下顺序进行查找:本地命名空间、全局命名空间、内置命名空间。
通过self
变量能访问到实例的命名空间(实例变量、私有实例变量和父类的实例变量)、类的命名空间(方法、类变量、私有方法和私有类变量)以及父类的命名空间(父类方法和父类的类变量)。这3种命名空间的查找顺序是:实例、类、父类。
通过self
变量无法访问到私有父类实例变量、私有父类方法和私有父类类变量。
析构函数和内存管理
上面已经介绍了类的初始化函数(__init__方法),还可以为类定义析构函数(Destructor)。但与C++语言不同,Python并不是一定要创建并调用析构函数,才能确保释放实例占用的内存。Python通过引用计数机制,提供了自动内存管理。也就是说,Python会跟踪实例的引用数量。当引用数为0时,实例占用的内存将会被回收,并且任何被实例引用的Python对象的引用计数都会减1。析构函数似乎始终都没有定义的必要。
在删除对象时,偶尔会碰到需要显式重新分配外部资源的场合,其最佳做法是使用上下文管理器with
。
数据类型即对象
Python的类型是动态确定的,也就意味着数据类型是在运行时确定的,而不是在编译时。这正是Python易于使用的原因之一,也使得可以用对象的类型(不只是对象本身)进行计算。
1
2
3
4
>>> type(5)
<class 'int'>
>>> type(['hello', 'goodbye'])
<class 'list'>
type
函数可以被任何Python对象调用,返回该对象的类型。从上面的代码中,我们可以看到int
和list
也是一种类型对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A:
pass
class B(A):
pass
>>> b = B()
>>> type(b) # b为类B的实例,定义在当前的__main__命名空间内
<class '__main__.B'>
>>> b.__class__ # 通过访问实例的特殊属性__class__也可以获取到完全一样的信息
<class '__main__.B'>
>>> b_class.__name__ # 通过__name___属性得到类的名称
'B'
>>> b_class.__bases__ # 通过访问__bases__属性,还可以找到类是从哪些类继承而来的
(<class '__main__.A'>,)
将__class__
、__bases__
和__name__
属性都一起用上,就能够对任一实例的类继承结构进行完整的分析了。不过isinstance
和issubclass
这两个内置函数提供了一种更加友好的手段,来获取大部分常用信息。
issubclass(class, classinfo)
函数用于判断一个类class
是否为另一个类classinfo
的子类。如果是,返回True,否则返回False。class
参数是一个类对象,classinfo
参数可以是一个类、元组或其他类型。如果classinfo
是一个元组,issubclass
函数会遍历元组中的每个元素,判断class
是否为这些元素中的任意一个的子类。isinstance(object, classinfo)
函数用于判断一个对象object
是否为一个类或其子类的实例。如果是,返回True,否则返回False。与issubclass
函数类似,classinfo
参数可以是一个类、元组或其他类型。
特殊方法属性
特殊方法属性(special method attribute)是Python类的一种属性,对Python而言具备特殊的含义。虽然被定义为方法,但其实并不是打算直接当作方法使用的。通常特殊方法不会被直接调用,而是由Python自动调用,以便对属于该类的对象的某种请求做出响应。
特殊方法属性最简单的例子是__str__
。如果是在类中定义的,只要Python请求该类的实例的可读字符串形式,就会调用__str__
方法属性,并将其返回值用作请求的字符串。
1
2
3
4
5
6
7
8
class Color:
def __init__(self, red, green, blue):
self._red = red
self._green = green
self._blue = blue
def __str__(self):
return "Color: R={0:d}, G={1:d}, B={2:d}".format(self._red, self._green, self._blue)
假如把上述类定义存入了color_module.py文件,就可以按常规方式导入并使用:
1
2
3
4
>>> from color_module import Color
>>> c = Color(15, 35, 3)
>>> print(c)
Color: R=15, G=35, B=3
即便没有任何代码对特殊方法属性__str__
发起显式的调用,Python还是会用到它的。Python知道__str__
属性(假如存在的话)定义了将对象转换为用户可读字符串的方法。这正是特殊方法属性定义的特色,能以专用方式定义挂入Python的钩子(hook)函数。
此外,还有__getitem__
等其他有用的特殊方法属性,这些特殊方法可以定义在类中,让该类的实例有能力展示特定的行为。
包
模块可以让小块代码轻松得以重新利用。当项目不断壮大,就会出现多种问题,需要重载的代码在物理或逻辑上都会超出单个文件的合理大小。解决这个问题的方案是把有关联的模块组合到同一个包中。
模块是容纳代码的文件,一个模块定义了一组Python函数和其他对象,通常这些函数和对象都是关联的。模块的名称由文件名称而来。
如果理解了模块,包就容易理解了,因为包就是包含代码和子目录的目录。包里包含了一组通常相互关联的代码文件(模块)。包的名称由主目录名而来。
包是模块概念的自然扩展,用于应对非常大型的项目。模块把相互关联的函数、类和变量进行了分组,同理,包则是把相互关联的模块进行了分组。
脚本文件是模块。
存储脚本文件的文件夹是包。
创建第一个包
考虑一种天生就十分庞大的项目设计,类似于Mathematica、Maple、MATLAB的通用数学计算包。例如,Maple就是由数千个文件组成的,代码的组织结构对于保持项目的井然有序至关重要。
整个项目命名为mathproj,这种项目的组织方式可以有很多种,一种比较合理的设计是把项目分为两部分。ui由UI部分组成,comp则包含了计算部分。在comp中,进一步把计算部分拆分为symbolic(实数和复数符号计算)和numeric(实数和复数数值计算)。
以下是一个简单的实现例子:
1
2
3
print("Hello from mathproj init")
__all__ = ['comp']
version = 1.03
1
2
__all__ = ['c1']
print("Hello from mathproj.comp init")
1
x = 1.00
1
print("Hello from numeric init")
1
2
3
4
5
6
from mathproj import version
from mathproj.comp import c1
from mathproj.comp.numeric.n2 import h
def g():
print("version is", version)
print(h())
1
2
def h():
return "Called function h in module n2"
包中的所有目录都会包含一个名为__init__.py
的文件,这个文件有两个用途。
- Python要求,只有包含
__init__.py
文件的目录才会被识别为包,可以防止意外导入包含其他Python代码的目录。很多包都不需要在其__init__.py
文件中写入任何内容,只要保证有个空的__init__.py
文件就行了。 - 当第一次加载包或子包时,Python会自动执行
__init__.py
文件。有了这种自动执行的机制,就能够完成任何必要的包初始化工作。
包的基本用法
新开一个Python shell,然后执行以下语句:
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import mathproj
Hello from mathproj init
>>> mathproj.version # 通过属性访问到其内部定义的对象
1.03
>>> import mathproj.comp.numeric.n1 # 子包和子模块需要显式手动导入
Hello from mathproj.comp init
Hello from numeric init
>>> mathproj.comp.numeric.n1.g() # 调用子模块中的函数
version is 1.03
Called function h in module n2
由于第一次加载包时会自动执行__init__.py
文件,因此屏幕上会打印出定义好的欢迎信息。
__all__属性
该属性与from ... import *
这类语句的执行有关,需要在此说明一下。
一般来说,如果外部代码执行了from mathproj import *
语句,就应该从mathproj导入全部的非私有对象名称。实际上这比较难以实现。主要问题是,有些操作系统对文件名的定义规则比较含糊。由于包中的对象可能是由文件或目录定义的,这就导致子包导入后其确切名称也含糊不定。例如,comp
会被导入为comp
、Comp
还是COMP
呢?如果想要只依赖操作系统给出的名称,那么结果可能是不可预测的。
上述问题没有很好的解决方案,这是由于操作系统设计欠佳造成的先天不足。作为最佳修正方案,Python引入了__all__
属性。如果__init__.py
文件中包含__all__
,__all__
应该给出一个字符串列表,定义对该包执行from ... import *
时应该导入的名称。如果未提供__all__
,则from ... import *
不会对该包执行任何操作。
以下是几条适用于大多数情况的包的代码建议:
- 包不应采用嵌套很深的目录结构。除非代码量极其庞大,否则没有必要这样做。大多数包只需要一个顶级目录即可。两层目录结构就应该能有效处理绝大部分情况。正如Tim Peters在《Python之禅》中所述,“平直胜于嵌套”。
- 只要不在
__all__
属性中列出,就可以用__all__
属性对from ... import *
隐藏这些对象名称。尽管如此,但这可能并不算是一种好方案,因为这会导致不同导入方式的结果不一致。如果需要隐藏对象名称,请用前缀双下划线让它们成为私有对象。
Python标准库
Python标准库非常庞大,所提供的组件涉及范围十分广泛,所有模块的目录可以参考官网的帮助文档,常用的模块如下目录所示:
字符串服务
主要包括处理字符串及文本、字节序列、Unicode操作。
模块 | 说明和应用场景 |
---|---|
string | 与数字或空白符这种字符串常量进行比较;格式化字符串 |
re | 用正则表达式查找和替换文本 |
struct | 将字节数据理解为打包的二进制数据,以及从文件读写结构化数据 |
数据类型
涵盖了各种各样的数据类型模块,特别是时间、日期和集合。
模块 | 说明和应用场景 |
---|---|
datetime、calendar | 日期、时间和日历操作 |
collections | 容器数据类型 |
enum | 允许创建枚举器类,将符号名称绑定到常量值上 |
array | 高效的数值型数组 |
sched | 事件调度器 |
queue | 同步队列 |
copy | 浅复制和深复制操作 |
pprint | 对数据进行美观打印 |
typing | 支持像对象类型提示那样的代码注释,特别是针对函数的参数和返回值 |
数值和数学运算
| 模块 | 说明和应用场景 | | ———– | ———————————— | | numbers | 数值对象的抽象基类 | | math、cmath | 实数和复数相关的数学函数 | | decimal | 十进制定点和浮点运算 | | statistics | 进行数学统计计算的函数 | | fractions | 有理数 | | random | 生成伪随机数,随机选取、打乱序列成员 | | itertools | 为高效循环创建迭代器的函数 | | functools | 针对可调用对象的高阶函数和操作 | | operator | 函数形式的标准运算符 |
文件和存储操作
包括文件访问模块、数据持久化和压缩模块和特殊文件格式处理模块。
模块 | 说明和应用场景 |
---|---|
os.path | 执行常见的路径名操作 |
pathlib | 以面向对象的方式处理路径名 |
fileinput | 从多个输入流迭代遍历数据行 |
filecmp | 比较文件和目录 |
tempfile | 生成临时文件和目录 |
glob、fnmatch | 采用UNIX风格的路径名和文件名模式处理 |
linecache | 实现对文本文件的随机访问 |
shutil | 执行高级文件操作 |
pickle、shelve | 提供Python对象序列化和持久化能力 |
sqlite3 | 操作SQLite数据库的DB-API 2.0接口 |
zlib、gzip、bz2、zipfile、tarfile | 操作归档文件及进行压缩 |
csv | 读写CSV文件 |
configparser | 使用配置文件解析器,读写 Windows 风格的 .ini 配置文件 |
操作系统服务
包括了很多工具库,例如,处理命令行参数、重定向文件及打印输出和输入、写入日志文件、运行多个线程或进程、加载供Python使用的非Python(通常为C)库。
模块 | 说明和应用场景 |
---|---|
os | 各种操作系统接口函数 |
io | 用于处理流的核心工具 |
time | 时间的访问和转换 |
optparse | 强大的命令行参数解析工具 |
logging | Python的日志记录工具 |
getpass | 可移植的密码输入工具 |
curses | 文本终端界面下用于控制字符区域的显示 |
platform | 访问底层平台的标识信息 |
ctypes | 让Python能调用外部函数库 |
select | 等待I/O完成 |
threading | 线程的高层接口 |
multiprocessing | 基于进程的线程接口 |
subprocess | 子进程的管理 |
互联网协议
对很多互联网数据交换标准格式进行编/解码,从MIME及其他编码、到JSON及XML。还包含为常见服务(尤其是HTTP)编写服务端和客户端的模块,以及为自定义服务编写通用套接字服务端。
模块 | 说明和应用场景 |
---|---|
socket、ssl | 底层网络接口及套接字对象的SSL封装 |
电子邮件和MIME处理包 | |
mailbox | 以各种格式处理邮箱 |
json | JSON编/解码 |
mimetypes | 将文件名映射为MIME类型 |
base64、binhex、binascii、quopri、uu | 用各种编码格式对文件或流进行编/解码 |
html.parser、html.entities | 解析HTML和XHTML |
xml.parsers.expat、xml.etree.ElementTree | XML处理模块 |
cgi、cgitb | CGI(Common Gateway Interface)支持 |
wsgiref | WSGI工具和参考实现 |
urllib.request、urllib.parse | URL解析器和工具 |
ftplib、poplib、imaplib、nntplib、smtplib、telnetlib | 各种互联网协议的客户端 |
socketserver | 网络服务端的框架 |
http.server | HTTP服务端 |
xmlrpc.client、xmlrpc.server | XML-RPC客户端和服务端 |
开发调试工具及运行时服务
帮助大家在运行时对Python代码进行调试、测试、修改和其他交互操作,该大类包括两个测试工具、多个性能分析器、与错误的跟踪信息(Traceback)进行交互的模块、解释器的垃圾回收器等,还包括可对其他模块的导入进行调整的模块。
模块 | 说明和应用场景 |
---|---|
pydoc | 文档生成器和在线帮助系统 |
doctest | 测试交互式Python程序 |
unittest | 单元测试框架 |
test.support | 用于测试的工具函数 |
pdb | Python调试器 |
profile、cProfile | Python性能分析器 |
timeit | 对代码片段进行运行计时 |
trace | 跟踪Python语句的执行过程 |
sys | 系统特有的参数和函数 |
atexit | 程序退出过程的处理 |
future | 定义未来语句,这是指将要加入Python的新特性 |
gc | 垃圾回收器接口 |
inspect | 查看活跃对象的信息 |
imp | 访问导入机制内部 |
zipimport | 从zip存档文件中导入模块 |
modulefinder | 查找脚本用到的所有模块 |
正则表达式库 re
search()和findall()
search()
函数用于接收一个正则表达式和一个字符串,并返回第一个匹配的字符串,示例如下。
1
2
3
4
5
6
7
8
9
import re
# search()的第一个参数为正则表达式,第二个参数为要处理的字符串
result = re.search(r'fox','the quick brown fox jumpred')
print(result.span()) # span()函数获取的是正则表达式匹配到的位置
print(result)
运行结果如下
(16, 19)
<re.Match object; span=(16, 19), match='fox'>
使用search()
函数会返回一个Match对象,调用Match对象的span()
函数可以获取正则表达式匹配到字符的位置,直接输出Match对象则会得到Match对象的描述信息,如果没有匹配到任何数据则会返回None。
如果想要获取匹配的结果,可以使用分组机制,对需要获取的结果添加()
,并使用group
属性提取:
1
2
3
4
5
6
7
8
9
10
11
12
13
content = 'Hello 123456789 Word_This is just a test 666 Test'
result = re.search('(\d+).*?(\d+).*', content)
print(result.group(0)) # 和 print(result.group()) 同样效果,返回re整体的匹配结果
print(result.group(1))
print(result.group(2))
print(result.groups())
运行结果如下
123456789 Word_This is just a test 666 Test
123456789
666
('123456789', '666')
findall()
函数用于获取多个匹配数据。示例如下。
1
2
3
4
5
6
import re
result = re.findall(r'张','张戈 张林 张东梅 张小凡')
print(result)
运行结果如下
['张', '张', '张', '张']
字符组[]
字符组[]
允许匹配一组可能出现的字符。
1
2
3
4
5
6
import re
result = re.findall(r'[Pp]ython','I like Python3 and I like python2.7 ')
print(result)
运行结果如下
['Python', 'python']
正则表达式中的方括号“[]”代表一组可能出现的字符组,一个字符组只能匹配一个字符。
区间[a-z]
在字符组中使用-
代表区间,例如:要匹配任意数字可以使用[0-9],要匹配所有小写英文字母可以使用[a-z]。
1
2
3
4
5
6
import re
a = re.findall(r'[a-z]','abc001ABC') # 匹配所有小写英文字母
print(a)
运行结果如下。
['a', 'b', 'c']
要单独匹配连字符的时候需要对它进行转义。在正则表达式中使用斜杠(\)可以对特殊符号进行转义,对连字符进行转义可以表示为\-
。
在方括号内,普通的正则表达式符号不会被解释。因此不需要在前面加上倒斜杠转义.
、*
、?
或()
字符。例如,[0-5.]
将匹配数字0~5和一个句点,不需要将它写成[0-5\.]
。
取反操作 ^
根据不会出现的字符来定义字符组,可以通过在字符组开头使用^
实现取反操作,从而可以反转一个字符组(意味着会匹配任何指定字符之外的所有字符)。
1
2
3
4
5
6
import re
result = re.findall(r'爱[^你]','我爱你 爱了 爱我自己 爱情')
print(result)
运行结果如下。
['爱了', '爱我', '爱情']
快捷方式 \w \d \s \b
正则表达式引擎提供了快捷方式,使用快捷方式匹配数据更加简洁。
\w
可以与包括下划线的任意单词字符匹配,w代表word,等价于[a-zA-Z0-9_]\d
可以与任意数字匹配,d代表digit,等价于[0-9]\s
可以与空白字符匹配,包括空格、制表符等,s代表space,等价于[\f\n\r\t\v]\b
可以匹配单词的边界
将上述字符大写,便可以实现取反的效果。例如\D
可以与任意非数字匹配,等价于[^0-9]
任意字符 .
.
代表匹配换行符\n
以外的任意单个字符,它只能出现在方括号以外(出现在方括号内表示匹配.
本身)。
1
2
3
4
5
6
7
8
import re
# 使用a..可以匹配任意以a开头的3个字符的数据
result = re.findall(r'a..','all ak47 abc and a')
print(result)
运行结果如下。
['all', 'ak4', 'abc', 'and']
可选字符 ?
有时候,我们可能想要匹配一个单词的不同写法,例如“color”和“colour”或者“honor”和“honour”。
这个时候可以使用?
指定一个字符或字符组是可选的,这意味着该字符会出现零次或一次。
1
2
3
4
5
6
import re
result = re.findall(r'欧?阳峰','欧阳峰 阳峰')
print(result)
运行结果如下。
['欧阳峰', '阳峰']
开始^和结尾$
在正则表达式的开始处使用^
,表明匹配必须发生在被查找文本开始处。
在正则表达式的末尾加上$
,表示该字符串必须以这个正则表达式的模式结束。
同时使用^
和$
,表明整个字符串必须匹配该模式。
1
2
3
4
5
>>> beginsWithHello = re.compile(r'^Hello')
>>> beginsWithHello.search('Hello world!')
<re. Match object; span=(0, 5), match='Hello'>
>>> beginsWithHello.search('He said hello.') == None
True
重复 {N}
当要匹配电话号码和身份证号等由多个数字组成的字符串时,我们需要使一个字符组连续匹配好几次。
在一个字符组后加上{N}
,就可以表示{N}
之前的字符组出现N次。
1
2
3
4
5
6
import re
# “\d{4}”和“\d{7}”分别表示匹配4次数字和匹配7次数字。
result = re.findall(r'\d{4}-\d{7}','张三0731-8825951,李四0733-8794561')
print(result)
运行结果如下。
['0731-8825951', '0733-8794561']
重复区间 {M,N}
有时候不知道重复的次数具体是多少,例如身份证号码有15位也有18位。这个时候可以使用重复区间{M,N}
。
1
2
3
4
5
import re
res = re.findall(r'\d{3,4}',' 020 0733')
print(res)
运行结果如下。
['020', '0733']
贪婪模式和非贪婪模式?
通过上述代码,可以发现\d{3,4}
既可以匹配3个数字也可以匹配4个数字,不过当有4个数字的时候,优先匹配的是4个数字,这是因为正则表达式默认是贪婪模式,即尽可能地匹配更多的字符。
要使用非贪婪模式,则需要在表达式后面加上?
。
1
2
3
4
5
import re
result = re.findall(r'\d{3,4}?',' 020 0733')
print(result)
运行结果如下。
['020', '073']
开闭区间 {N,}
有时候可能会遇到字符组的重复次数没有边界的情况,例如从1个到无穷个,这个时候可以使用开闭区间{N,}
。
1
2
3
4
5
import re
result = re.findall(r'\d{1,}','1 20 020 0733')
print(result)
运行结果如下。
['1', '20', '020', '0733']
匹配无穷个 * +
在正则表达式中有两个使用频率非常高的符号:
+
代表匹配1个到无穷个,等价于{1,}
*
代表匹配0个到无穷个,等价于{0,}
1
2
3
4
5
6
7
8
import re
result1 = re.findall(r'\d+','1 20 020 0733')
result2 = re.findall(r'编号\d*','编号 编号89757')
print(result1)
print(result2)
运行结果如下。
['1', '20', '020', '0733']
['编号', '编号89757']
分组 ()
在正则表达式中还提供了一种将表达式分组的机制,使用该机制时,除了获得所有匹配结果,还能够在匹配结果中选择每一个分组。
1
2
3
4
5
6
7
8
9
import re
result = re.search(r'([\d]{4})-([\d]{7})','张三:0731-8825951')
print(result.group())
print(result.group(1))
print(result.group(2))
运行结果如下。
0731-8825951
0731
8825951
([\d]{4})-([\d]{7})
将结果分为了两组,使用group()
函数可以得到所有匹配的结果,使用group(N)
可以得到第N个分组。
在findall()
函数中也可以实现分组,但findall()函数会直接返回分组提取的结果。
1
2
3
4
5
import re
result = re.findall(r'([\d]{4})-([\d]{7})','张三:0731-8825951')
print(result)
运行结果如下。
[('0731', '8825951')]
或者 |
使用分组的同时还可以使用或者条件,要使用或者条件可以在各个条件之间加上|
。
1
2
3
4
5
import re
result= re.findall(r'(张三|李四)','张三、李四、王五、赵六')
print(result)
运行结果如下。
['张三', '李四']
分组的回溯引用 \N
正则表达式还提供了一种引用之前匹配分组的机制,示例如下。
1
2
3
4
5
import re
result = re.findall(r'<[\w_-]+>.*?</[\w_-]+>','0123<font>提示</font>abcd')
print(result)
运行结果如下。
<font>提示</font>
运行上述代码确实可以得到匹配结果,不过如果解析数据为0123<font>提示</bar>abcd
,则结果可能并不符合我们的预期。
1
2
3
4
5
import re
res = re.findall(r'<[\w_-]+>.*?</[\w_-]+>','0123<font>提示</bar>abcd')
print(res)
运行结果如下。
['<font>提示</bar>']