Python
一、基础
0.前言
感谢廖雪峰老师提供这么好的教程,此文档为本人学习中所做笔记
以#
开头的语句是注释。其他每一行都是一个语句,当语句以冒号:
结尾时,缩进的语句视为代码块。
Python程序是大小写敏感的,如果写错了大小写,程序会报错。
按照约定俗成的惯例,应该始终坚持使用4个空格的缩进。
1.数据类型和变量
1.1数据类型
-
整数
- Python的整数没有大小限制,而某些语言的整数根据其存储长度是有大小限制的
-
浮点数
- Python的浮点数也没有大小限制,但是超出一定范围就直接表示为
inf
(无限大)。
- Python的浮点数也没有大小限制,但是超出一定范围就直接表示为
-
字符串
-
字符串是以单引号
'
或双引号"
括起来的任意文本,比如'abc'
,"xyz"
等等。 -
为了简化,Python还允许用
r''
表示''
内部的字符串默认不转义1
2
3
4print('\\\t\\')
\ \
print(r'\\\t\\')
\\\t\\ -
为了简化,Python允许用
'''...'''
的格式表示多行内容1
2
3
4
5
6print('''line1
line2
) line3'''
line1
line2
line3上面是在交互式命令行内输入,注意在输入多行内容时,提示符由
>>>
变为...
,提示你可以接着上一行输入,注意...
是提示符,不是代码的一部分 -
多行字符串
'''...'''
还可以在前面加上r
使用1
2
3
4
5print(r'''hello,\n
world''')
#结果如下
hello,\n
world
-
-
布尔值
- 布尔值只有
True
、False
两种值 - 布尔值可以用
and
、or
和not
运算
- 布尔值只有
-
空值
- 空值是Python里一个特殊的值,用
None
表示 None
不能理解为0
,因为0
是有意义的,而None
是一个特殊的空值。
- 空值是Python里一个特殊的值,用
1.2变量
可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量
这种 变量本身类型 不固定的语言称之为动态语言,与之对应的是静态语言
静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错
1.3常量
常量就是不能变的变量。在Python中,通常用全部大写的变量名表示常量:
1 | PI = 3.14159265359 |
但事实上PI
仍然是一个变量,Python根本没有任何机制保证PI
不会被改变
1.4除法相关
在Python中,有两种除法,一种除法是/
:
1 | 10 / 3 |
/
除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数
还有一种除法是//
,称为地板除,两个整数的除法仍然是整数:
1 | 10 // 3 |
因为//
除法只取结果的整数部分,所以Python还提供一个余数运算,可以得到两个整数相除的余数:
1 | 10 % 3 |
2.字符串和编码
2.0前言 字符编码
历史关系
捋一捋ASCII编码和Unicode编码的区别:ASCII编码是1个字节,而Unicode编码通常是2个字节。
字母A
用ASCII编码是十进制的65
,二进制的01000001
;
字符0
用ASCII编码是十进制的48
,二进制的00110000
,注意字符'0'
和整数0
是不同的;
汉字中
已经超出了ASCII编码的范围,用Unicode编码是十进制的20013
,二进制的01001110 00101101
。
你可以猜测,如果把ASCII编码的A
用Unicode编码,只需要在前面补0就可以,因此,A
的Unicode编码是00000000 01000001
。
新的问题又出现了:如果统一成Unicode编码,乱码问题从此消失了。但是,如果你写的文本基本上全部是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。
所以,本着节约的精神,又出现了把Unicode编码转化为“可变长编码”的UTF-8
编码。UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。如果你要传输的文本包含大量英文字符,用UTF-8编码就能节省空间:
字符 | ASCII | Unicode | UTF-8 |
---|---|---|---|
A | 01000001 | 00000000 01000001 | 01000001 |
中 | x | 01001110 00101101 | 11100100 10111000 10101101 |
从上面的表格还可以发现,UTF-8编码有一个额外的好处,就是ASCII编码实际上可以被看成是UTF-8编码的一部分,所以,大量只支持ASCII编码的历史遗留软件可以在UTF-8编码下继续工作。
计算机系统通用的字符编码工作方式
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件:
浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器:
所以你看到很多网页的源码上会有类似<meta charset="UTF-8" />
的信息,表示该网页正是用的UTF-8编码。
2.1Python的字符串
在最新的Python 3版本中,字符串是以Unicode编码的
对于单个字符的编码,Python提供了ord()
函数获取字符的整数表示,chr()
函数把编码转换为对应的字符:
1 | ord('A') |
如果知道字符的整数编码,还可以用十六进制这么写str
:
1 | '\u4e2d\u6587' |
两种写法完全是等价的。
由上文可知,Python的字符串在内存中以Unicode表示,如果要在网络上传输,或者保存到磁盘上,就需要把str
变为以字节为单位的bytes
。
Python对bytes
类型的数据用带b
前缀的单引号或双引号表示:
1 | x = b'ABC' |
要注意'ABC'
和b'ABC'
,虽然内容显示一样,但bytes
的每个字符都只占用一个字节。
以Unicode表示的str
通过encode()
方法可以编码为指定的bytes
,例如:
1 | 'ABC'.encode('ascii') |
在bytes
中,无法显示为ASCII字符的字节,用\x##
显示。
反过来,要把bytes
变为str
,就需要用decode()
方法:
1 | b'ABC'.decode('ascii') |
如果bytes
中包含无法解码的字节,decode()
方法会报错
如果bytes
中只有一小部分无效的字节,可以传入errors='ignore'
忽略错误的字节:
1 | b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore') |
要计算str
包含多少个字符,可以用len()
函数,如果将str
换成bytes
,len()
函数就计算字节数
- 1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。
在操作字符串时,我们经常遇到str
和bytes
的互相转换。为了避免乱码问题,应当始终坚持使用UTF-8编码对str
和bytes
进行转换。
当你的源代码中包含中文的时候,就需要务必指定保存为UTF-8编码。我们通常在文件开头写上这两行:
1 | #!/usr/bin/env python3 |
-
第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
-
第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。
2.2格式化
和C语言是一致的,用%
实现:
1 | 'Hello, %s' % 'world' |
在字符串内部,有几个%?
占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?
,括号可以省略。
常见的占位符有:
占位符 | 替换内容 |
---|---|
%d | 整数 |
%f | 浮点数 |
%s | 字符串 |
%x | 十六进制整数 |
其中,格式化整数和浮点数还可以指定是否补0和整数与小数的位数:
1 | print('%2d-%02d' % (3, 1)) |
如果你不太确定应该用什么,%s
永远起作用,它会把任何数据类型转换为字符串:
1 | 'Age: %s. Gender: %s' % (25, True) |
当需要显示%
时,用%%
来转义成%
format()
另一种格式化字符串的方法是使用字符串的format()
方法,它会用传入的参数依次替换字符串内的占位符{0}
、{1}
……,不过这种方式写起来比%要麻烦得多:
1 | 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125) |
3.使用list和tuple
3.1list列表
list,列表,是一种有序的集合,可以随时添加和删除其中的元素。
用len()
函数可以获得list元素的个数
基础操作类似于数组
1 | #创建 |
增加,删除:
1 | #追加元素到末尾 |
list里面的元素的数据类型可以不同:
1 | 'Apple', 123, True] L = [ |
空的list,长度为0
注意:
list实际上可以理解为一个指向数组的指针,也就是说list和tuple的底层实现有点类似(见后文中的 tuple的本质 章节),所以普通的list赋值是指针的赋值:
1 | a = [1,2,3] |
3.2tuple元组
另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,所以没有append(),insert()这样的方法。其他获取元素的方法和list是一样。
因为tuple不可变,所以代码更安全。如果可能,能用tuple代替list就尽量用tuple。
tuple的陷阱:当你定义一个tuple时,在定义的时候,tuple的元素就必须被确定下来,比如:
1 | 1, 2) t = ( |
注意:只有1个元素的tuple定义时为了和数学公式中的小括号区分,必须加一个逗号,
来消除歧义:
1 | 1,) t = ( |
3.2.1tuple的本质
tuple的本质有点类似C语言中的const *
,即tuple的每个元素,指向永远不变。
例子:
1 | 'a', 'b', ['A', 'B']) t = ( |
这个tuple定义的时候有3个元素,分别是'a'
,'b'
和一个list。但是tuple最后却变了,为什么?
我们先看看定义的时候tuple包含的3个元素:
当我们把list的元素'A'
和'B'
修改为'X'
和'Y'
后,tuple变为:
表面上看,tuple的元素确实变了,但其实变的不是tuple的元素,而是list的元素。tuple一开始指向的list并没有改成别的list,所以,tuple所谓的“不变”是说,tuple的每个元素,指向永远不变。即指向'a'
,就不能改成指向'b'
,指向一个list,就不能改成指向其他对象,但指向的这个list本身是可变的!
要创建一个内容也不变的tuple怎么做?那就必须保证tuple的每一个元素本身也不能变。
4.条件判断
4.1if语句
elif
是else if
的缩写,可以有多个elif
,if
语句的完整形式是:
1 | if <条件判断1>: |
if
判断条件还可以简写,比如写:
1 | if x: |
只要x
是非零数值、非空字符串、非空list等,就判断为True
,否则为False
。
4.2input
用input()
读取用户的输入,这样可以自己输入,程序运行得更有意思。
注意:input()
返回的数据类型是str
,str
不能直接和整数比较,必须先把str
转换成整数。Python提供了int()
函数来完成这件事情:
1 | s = input('birth: ') |
5.循环
Python的循环有两种,一种是for…in循环,依次把list或tuple中的每个元素迭代出来,即for x in ...
循环就是把每个元素代入变量x
,然后执行缩进块的语句。
Python提供一个range()
函数,可以生成一个整数序列,再通过list()
函数可以转换为list。
1 | list(range(5)) |
第二种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。
1 | sum = 0 |
在循环中,break
语句可以提前退出循环。
在循环过程中,也可以通过continue
语句,跳过当前的这次循环,直接开始下一次循环。
6.使用dict和set
6.1dict字典
Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。
1 | #初始化 |
如果key不存在,dict就会报错
要避免key不存在的错误,有两种办法,一是通过in
判断key是否存在:
1 | 'Thomas' in d |
二是通过dict提供的get()
方法,如果key不存在,可以返回None
,或者自己指定的value:
1 | 'Thomas') d.get( |
注意:
- 返回
None
的时候Python的交互环境不显示结果。 - dict内部存放的顺序和key放入的顺序是没有关系的。
- dict的key必须是不可变对象。(哈希算法,利用key计算value的位置)
- 字符串、整数等都是不可变的,而list是可变的,不能作为key
和list比较,dict有以下几个特点:
- 查找和插入的速度极快,不会随着key的增加而变慢;
- 需要占用大量的内存,内存浪费多。
而list相反:
- 查找和插入的时间随着元素的增加而增加;
- 占用空间小,浪费内存很少。
6.2set
set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。
要创建一个set,需要提供一个list作为输入集合:
1 | set([1, 2, 3]) s = |
注意,传入的参数[1, 2, 3]
是一个list,而显示的{1, 2, 3}
只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。。
基本操作:
1 | #添加 可以重复添加,但不会有效果 |
set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:
1 | set([1, 2, 3]) s1 = |
set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。
6.3不可变对象
上面我们讲了,str是不变对象,而list是可变对象。
对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:
1 | 'c', 'b', 'a'] a = [ |
而对于不可变对象,比如str,对str进行操作呢:
1 | 'abc' a = |
虽然字符串有个replace()
方法,也确实变出了'Abc'
,但变量a
最后仍是'abc'
,应该怎么理解呢?
我们先把代码改成下面这样:
1 | 'abc' a = |
要始终牢记的是,a
是变量,而'abc'
才是字符串对象!有些时候,我们经常说,对象a
的内容是'abc'
,但其实是指,a
本身是一个变量,它指向的对象的内容才是'abc'
:
1 | ┌───┐ ┌───────┐ |
当我们调用a.replace('a', 'A')
时,实际上调用方法replace
是作用在字符串对象'abc'
上的,而这个方法虽然名字叫replace
,但却没有改变字符串'abc'
的内容。相反,replace
方法创建了一个新字符串'Abc'
并返回,如果我们用变量b
指向该新字符串,就容易理解了,变量a
仍指向原有的字符串'abc'
,但变量b
却指向新字符串'Abc'
了:
1 | ┌───┐ ┌───────┐ |
所以,对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。
7.*号
数学运算中, *
为乘,**
为次方。
当要使函数接收元组或字典形式的参数的时候,有一种特殊的方法,它分别使用*
和**
前缀 。这种方法在函数需要获取可变数量的参数的时候特别有用。
7.1*号
单个星号:这个位置接收任意多个非关键字参数,并转化成元表。
1 | import collections |
*b
会接受除了a
之外的剩下的非关键字参数- 注意:
*
加在形参面前代表的是收集参数,如果*
号加在了是实参上(例如第十四行),代表的是将输入迭代器拆成一个个元素
7.2**号
双星号:这个位置接收任意多个关键字参数,并按照关键字转化成字典
1 | def three(**b): |
- 用双星号传入实参的时候,一定是所有的实参必须带有关键字
二、函数
1.调用函数
Python的官方网站查看内置函数文档:https://docs.python.org/zh-cn/3/library/functions.html
1.1数据类型转换
Python内置的常用函数还包括数据类型转换函数,比如int()
函数可以把其他数据类型转换为整数,hex()
函数把一个整数转换成十六进制表示的字符串
函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
1 | abs # 变量a指向abs函数 a = |
2.定义函数
在Python中,定义一个函数要使用def
语句,依次写出函数名、括号、括号中的参数和冒号:
,然后,在缩进块中编写函数体,函数的返回值用return
语句返回。
1 | def my_abs(x): |
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为None
。return None
可以简写为return
。
2.1空函数
如果想定义一个什么事也不做的空函数,可以用pass
语句:
1 | def nop(): |
pass
语句可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass
,让代码能运行起来。
2.2返回多个值
例如:
1 | import math |
但其实这只是一种假象,Python函数返回的仍然是单一值:
1 | #看起来是两个值 |
在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。
3函数的参数
3.1位置参数
1 | def power(x): |
对于power(x)
函数,参数x
就是一个位置参数。
当我们调用power
函数时,必须传入有且仅有的一个参数x
修改原函数后的power(x, n)
函数,可以计算任意n次方:
1 | def power(x, n): |
修改后的power(x, n)
函数有两个参数:x
和n
,这两个参数都是位置参数,调用函数时,传入的两个值按照位置顺序依次赋给参数x
和n
。
3.2默认参数
新的power(x, n)
函数定义没有问题,但是,旧的调用代码失败了,原因是我们增加了一个参数,导致旧的代码因为缺少一个参数而无法正常调用
由于我们经常计算x2,所以,完全可以把第二个参数n的默认值设定为2:
1 | def power(x, n=2): |
这样,当我们调用power(5)
时,相当于调用power(5, 2)
默认参数可以简化函数的调用,但是需要注意:
- 必选参数在前,默认参数在后,否则Python的解释器会报错
- 当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。
- 默认参数在函数定义的时候就被计算出来了,且必须指向一个不变对象,见后文例子
有多个默认参数时,调用的时候,既可以按顺序提供默认参数,也可以不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。
默认参数例子:
1 | def add_end(L=[]): |
当你正常调用时,结果似乎不错:
1 | 1, 2, 3]) add_end([ |
Python函数在定义的时候,默认参数L
的值就被计算出来了,即[]
,因为默认参数L
也是一个变量,它指向对象[]
,每次调用该函数,如果改变了L
的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]
了。
定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用None
这个不变对象来实现:
1 | def add_end(L=None): |
现在,无论调用多少次,都不会有问题:
3.3可变参数
1 | def calc(*numbers): |
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*
号。在函数内部,参数numbers
接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数
如果已经有一个list或者tuple,Python允许你在list或tuple前面加一个*
号,把list或tuple的元素变成可变参数传进去:
1 | 1, 2, 3] nums = [ |
*nums
表示把nums
这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
3.4关键字参数
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:
1 | def person(name, age, **kw): |
函数person
除了必选参数name
和age
外,还接受关键字参数kw
。在调用该函数时,可以只传入必选参数:
1 | #只传入必选参数 |
和可变参数类似,已经有一个dict,Python允许你在dict前面加一个**
号,把dict的元素变成可变参数传进去:
1 | 'city': 'Beijing', 'job': 'Engineer'} extra = { |
3.5命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw
检查。
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city
和job
作为关键字参数。这种方式定义的函数如下:
1 | def person(name, age, *, city, job): |
和关键字参数**kw
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。
调用方式如下:
1 | 'Jack', 24, city='Beijing', job='Engineer') person( |
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
1 | def person(name, age, *args, city, job): |
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错
命名关键字参数也可以有缺省值,从而简化调用
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*
作为特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数:
1 | def person(name, age, city, job): |
3.6组合参数
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
比如定义一个函数,包含上述若干种参数:
1 | def f1(a, b, c=0, *args, **kw): |
在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。
1 | 1, 2) f1( |
最神奇的是通过一个tuple和dict,你也可以调用上述函数:
1 | #调用f1 |
所以,对于任意函数,都可以通过类似func(*args, **kw)
的形式调用它,无论它的参数是如何定义的。
虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。
4递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
1 | #没有尾递归优化 |
遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)
函数改成尾递归方式,也会导致栈溢出。
三、高级特性
3.1切片
对这种经常取指定索引范围的操作,用循环十分繁琐,因此,Python提供了切片(Slice)操作符,能大大简化这种操作。
对应上面的问题,取前3个元素,用一行代码就可以完成切片:
1 | 'Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] L = [ |
L[0:3]
表示,从索引0
开始取,直到索引3
为止,但不包括索引3
。即索引0
,1
,2
,正好是3个元素。
1 | #创建一个更大的数列 |
tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple
同样,字符串'xxx'
也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串
3.2迭代
如果给定一个list或tuple,我们可以通过for
循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)。
在Python中,迭代是通过for ... in
来完成的,而很多语言比如C语言,迭代list是通过下标完成的
list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代
1 | 'a': 1, 'b': 2, 'c': 3} d = { |
因为dict的存储不是按照list的方式顺序排列,所以,迭代出的结果顺序很可能不一样。
默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values()
,如果要同时迭代key和value,可以用for k, v in d.items()
。
由于字符串也是可迭代对象,因此,也可以作用于for
循环
通过collections模块的Iterable类型可以判断一个对象是否为可迭代对象:
1 | from collections import Iterable |
Python内置的enumerate
函数可以把一个list变成索引-元素对,这样就可以在for
循环中同时迭代索引和元素本身:
1 | for i, value in enumerate(['A', 'B', 'C']): |
上面的for
循环里,同时引用了两个变量,在Python里是很常见的
3.3列表生成式
列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。
1 | #普通方法 |
3.4生成器
如果列表元素可以按照某种算法推算出来,那我们可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]
改成()
,就创建了一个generator:
1 | for x in range(10)] L = [x * x |
创建L
和g
的区别仅在于最外层的[]
和()
,L
是一个list,而g
是一个generator。
generator保存的是算法,每次调用next(g)
,就计算出g
的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration
的错误。
generator是可迭代对象,所以能够使用for
循环迭代
我们创建了一个generator后,基本上永远不会调用next()
,而是通过for
循环来迭代它,并且不需要关心StopIteration
的错误。
定义generator的另一种方法。如果一个函数定义中包含yield
关键字,那么这个函数就不再是一个普通函数,而是一个generator:
1 | #普通函数 生成斐波拉契数列 |
其中赋值语句:
1 a, b = b, a + b相当于:
1
2
3 t = (b, a + b) # t是一个tuple
a = t[0]
b = t[1]但不必显式写出临时变量t就可以赋值。
这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return
语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。可概括为以下三点:
- 生成器是一个不断产生值的函数
- 包含yield语句的函数是一个生成器
- 生成器每次产生一个值(yield语句),函数被冻结,被唤醒后再产生一个值
例子,定义一个generator,依次返回数字1,3,5:
1
2
3
4
5
6
7 def odd():
print('step 1')
yield 1
print('step 2')
yield(3)
print('step 3')
yield(5)调用该generator时,首先要生成一个generator对象,然后用
next()
函数不断获得下一个返回值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 o = odd()
next(o)
step 1
1
next(o)
step 2
3
next(o)
step 3
5
next(o)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration可以看到,
odd
不是普通函数,而是generator,在执行过程中,遇到yield
就中断,下次又继续执行。执行3次yield
后,已经没有yield
可以执行了,所以,第4次调用next(o)
就报错。
用for
循环调用generator时(如这种:for n in fib(6)
),发现拿不到generator的return
语句的返回值。如果想要拿到返回值,必须捕获StopIteration
错误,返回值包含在StopIteration
的value
中:
1 | 6) g = fib( |
3.5迭代器
可以直接作用于for
循环的数据类型有以下几种:
一类是集合数据类型,如list
、tuple
、dict
、set
、str
等;
一类是generator
,包括生成器和带yield
的generator function。
这些可以直接作用于for
循环的对象统称为可迭代对象:Iterable
。
可以使用isinstance()
判断一个对象是否是Iterable
对象:
1 | from collections import Iterable |
而生成器不但可以作用于for
循环,还可以被next()
函数不断调用并返回下一个值,直到最后抛出StopIteration
错误表示无法继续返回下一个值了。
可以被next()
函数调用并不断返回下一个值的对象称为迭代器:Iterator
。
可以使用isinstance()
判断一个对象是否是Iterator
对象:
1 | from collections import Iterator |
生成器都是Iterator
对象,但list
、dict
、str
虽然是Iterable
,却不是Iterator
。
把list
、dict
、str
等Iterable
变成Iterator
可以使用iter()
函数:
1 | isinstance(iter([]), Iterator) |
为什么list
、dict
、str
等数据类型不是Iterator
?
这是因为Python的Iterator
对象表示的是一个数据流,Iterator对象可以被next()
函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration
错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()
函数实现按需计算下一个数据,所以Iterator
的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator
甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
总之:
- 凡是可作用于
for
循环的对象都是Iterable
类型; - 凡是可作用于
next()
函数的对象都是Iterator
类型,它们表示一个惰性计算的序列; - Python的
for
循环本质上就是通过不断调用next()
函数实现的- 集合数据类型如
list
、dict
、str
等会先通过iter()
函数获得一个Iterator
对象
- 集合数据类型如
四、函数式编程
4.1高阶函数
函数本身也可以赋值给变量,即:变量可以指向函数。
如果一个变量指向了一个函数,那么,可以通过该变量来调用这个函数
1 | abs f = |
函数名其实就是指向函数的变量!对于abs()
这个函数,完全可以把函数名abs
看成变量,它指向一个可以计算绝对值的函数!
1 | abs = 10#修改abs的指向 |
注意:由于abs
函数实际上是定义在import builtins
模块中的,所以要让修改abs
变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10
。
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
如:
1 | def add(x, y, f): |
编写高阶函数,就是让函数的参数能够接收别的函数。函数式编程就是指这种高度抽象的编程范式。
4.1.1map/reduce
Python内建了map()
和reduce()
函数。
map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
比如我们有一个函数f(x)=x2,要把这个函数作用在一个list
[1, 2, 3, 4, 5, 6, 7, 8, 9]
上,就可以用map()
实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 f(x) = x * x
│
│
┌───┬───┬───┬───┼───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[ 1 2 3 4 5 6 7 8 9 ]
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[ 1 4 9 16 25 36 49 64 81 ]现在,我们用Python代码实现:
1
2
3
4
5
6 def f(x):
return x * x
...
map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]) r =
list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
map()
传入的第一个参数是f
,即函数对象本身。由于结果r
是一个Iterator
,Iterator
是惰性序列,因此通过list()
函数让它把整个序列都计算出来并返回一个list。
map()
作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x^2^,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:
1 | list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
只需要一行代码。
reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,其效果就是:
1 | reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) |
比如对一个序列求和,就可以用
reduce
实现:
1
2
3
4
5
6 from functools import reduce
def add(x, y):
return x + y
...
1, 3, 5, 7, 9]) reduce(add, [
25比如把
str
转换为int
:
1
2
3
4
5
6
7
8
9
10 from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def str2int(s):
def fn(x, y):
return x * 10 + y
def char2num(s):
return DIGITS[s]
return reduce(fn, map(char2num, s))还可以用lambda函数进一步简化成:
1
2
3
4
5
6
7
8
9 from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def char2num(s):
return DIGITS[s]
def str2int(s):
return reduce(lambda x, y: x * 10 + y, map(char2num, s))lambda函数的用法在后面介绍。
4.1.2filter
和map()
类似,filter()
也接收一个函数和一个序列。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素。
把一个序列中的空字符串删掉:
1
2
3
4
5 def not_empty(s):
return s and s.strip()
list(filter(not_empty, ['A', '', 'B', None, 'C', ' ']))
# 结果: ['A', 'B', 'C']
注意到filter()
函数返回的是一个Iterator
,也就是一个惰性序列,所以要强迫filter()
完成计算结果,需要用list()
函数获得所有结果并返回list。
4.1.3sorted
Python内置的sorted()
函数就可以对list从小到大进行排序:
1 | sorted([36, 5, -12, 9, -21]) |
此外,sorted()
函数也是一个高阶函数,它还可以接收一个key
函数来实现自定义的排序,key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。
如按绝对值大小排序:
1
2 sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]这个过程可以理解为:
1
2 list = [36, 5, -12, 9, -21]#原始的list
keys = [36, 5, 12, 9, 21]#经过key=abs处理过的list然后
sorted()
函数按照keys进行排序,并按照对应关系返回list相应的元素:
1
2
3 keys排序结果 => [5, 9, 12, 21, 36]
| | | | |
最终结果 => [5, 9, -12, -21, 36]
默认情况下,对字符串排序,是按照ASCII的大小比较的
要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True
4.2返回函数
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
1 | def lazy_sum(*args): |
当我们调用lazy_sum()
时,返回的并不是求和结果,而是求和函数:
1 | 1, 3, 5, 7, 9) f = lazy_sum( |
调用函数f
时,才真正计算求和的结果
在这个例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
1 | 1, 3, 5, 7, 9) f1 = lazy_sum( |
f1()
和f2()
的调用结果互不影响。
4.2.1闭包
注意到返回的函数在其定义内部引用了局部变量args
,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()
才执行。
例子:
1
2
3
4
5
6
7
8
9 def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
你可能认为调用
f1()
,f2()
和f3()
结果应该是1
,4
,9
,但实际结果是:
1
2
3
4
5
6 f1()
9
f2()
9
f3()
9全部都是
9
!原因就在于返回的函数引用了变量i
,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i
已经变成了3
,因此最终结果为9
。
返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
1
2
3
4
5
6
7
8
9 def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs再看看结果:
1
2
3
4
5
6
7 f1, f2, f3 = count()
f1()
1
f2()
4
f3()
9缺点是代码较长,可利用lambda函数缩短代码。
4.2.2关键字global与nonlocal
第一,两者的功能不同。global关键字修饰变量后标识该变量是全局变量,对该变量进行修改就是修改全局变量,而nonlocal关键字修饰变量后标识该变量是上一级函数中的局部变量,如果上一级函数中不存在该局部变量,nonlocal位置会发生错误(最上层的函数使用nonlocal修饰变量必定会报错)。
第二,两者使用的范围不同。global关键字可以用在任何地方,包括最上层函数中和嵌套函数中,即使之前未定义该变量,global修饰后也可以直接使用,而nonlocal关键字只能用于嵌套函数中,并且外层函数中定义了相应的局部变量,否则会发生错误
4.3匿名函数
匿名函数lambda x: x * x
实际上就是:
1 | def f(x): |
关键字lambda
表示匿名函数,冒号前面的x
表示函数参数。
匿名函数有个限制,就是只能有一个表达式,不用写return
,返回值就是该表达式的结果。
用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
1 | lambda x: x * x f = |
同样,也可以把匿名函数作为返回值返回,比如:
1 | def build(x, y): |
4.4装饰器
函数对象有一个__name__
属性,可以拿到函数的名字
1 | def now(): |
现在,假设我们要增强now()
函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()
函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:
1 | def log(func): |
观察上面的log
,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的**@**语法,把decorator置于函数的定义处:
1 |
|
调用now()
函数,不仅会运行now()
函数本身,还会在运行now()
函数前打印一行日志:
1 | now() |
把@log
放到now()
函数的定义处,相当于执行了语句:
1 | now = log(now) |
由于log()
是一个decorator,返回一个函数,所以,原来的now()
函数仍然存在,只是现在同名的now
变量指向了新的函数,于是调用now()
将执行新函数,即在log()
函数中返回的wrapper()
函数。
wrapper()
函数的参数定义是(*args, **kw)
,因此,wrapper()
函数可以接受任意参数的调用。在wrapper()
函数内,首先打印日志,再紧接着调用原始函数。
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
1 | def log(text): |
这个3层嵌套的decorator用法如下:
1 |
|
执行结果如下:
1 | now() |
和两层嵌套的decorator相比,3层嵌套的效果是这样的:
1 | 'execute')(now) now = log( |
我们来剖析上面的语句,首先执行log('execute')
,返回的是decorator
函数,再调用返回的函数,参数是now
函数,返回值最终是wrapper
函数。
以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__
等属性,但你去看经过decorator装饰之后的函数,它们的__name__
已经从原来的'now'
变成了'wrapper'
:
1 | now.__name__ |
因为返回的那个wrapper()
函数名字就是'wrapper'
,所以,需要把原始函数的__name__
等属性复制到wrapper()
函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ = func.__name__
这样的代码,Python内置的functools.wraps
就是干这个事的,所以,一个完整的decorator的写法如下:
1 | import functools |
或者针对带参数的decorator:
1 | import functools |
import functools
是导入functools
模块。模块的概念稍候讲解。现在,只需记住在定义wrapper()
的前面加上@functools.wraps(func)
即可。
4.5偏函数
Python的functools
模块提供了很多有用的功能,其中一个就是偏函数(Partial function)
functools.partial
就是帮助我们创建一个偏函数的,作用就是把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
1 | import functools |
最后,创建偏函数时,实际上可以接收 函数对象、*args
和**kw
这3个参数
例子:
1
2
3
4
5 int2 = functools.partial(int, base=2)
int2('10010')
#相当于
kw = { 'base': 2 }
int('10010', **kw)又:
1
2
3
4
5 max2 = functools.partial(max, 10)
max2(5, 6, 7)
#相当于
args = (10, 5, 6, 7)#实际上会把10作为*args的一部分自动加到左边
max(*args)注意上述例子中会把10作为*args的一部分自动加到左边
五、模块
5.0模块和包
在Python中,一个.py文件就称之为一个模块(Module)。
为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。
例子:
一个
abc.py
的文件就是一个名字叫abc
的模块,一个xyz.py
的文件就是一个名字叫xyz
的模块。现在,假设我们的
abc
和xyz
这两个模块名字与其他模块冲突了,于是我们可以通过包来组织模块,避免冲突。方法是选择一个顶层包名,比如mycompany
,按照如下目录存放:
1
2
3
4 mycompany
├─ __init__.py
├─ abc.py
└─ xyz.py引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。现在,
abc.py
模块的名字就变成了mycompany.abc
,类似的,xyz.py
的模块名变成了mycompany.xyz
。请注意,每一个包目录下面都会有一个
__init__.py
的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py
可以是空文件,也可以有Python代码,因为__init__.py
本身就是一个模块,而它的模块名就是mycompany
。
可以有多级目录,组成多级层次的包结构
例子:
1
2
3
4
5
6
7
8 mycompany
├─ web
│ ├─ __init__.py
│ ├─ utils.py
│ └─ www.py
├─ __init__.py
├─ abc.py
└─ utils.py文件
www.py
的模块名就是mycompany.web.www
,两个文件utils.py
的模块名分别是mycompany.utils
和mycompany.web.utils
。
mycompany.web
也是一个模块,该模块对应为mycompany.web.__init__.py
文件。
自己创建模块时要注意命名,不能和Python自带的模块名称冲突。例如,系统自带了sys模块,自己的模块就不可命名为sys.py,否则将无法导入系统自带的sys模块。
5.1使用模块
Python模块的标准文件模板:
1 | #!/usr/bin/env python3 |
第1行和第2行是标准注释,第1行注释可以让这个hello.py
文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码;
第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;
第6行使用__author__
变量把作者写进去
使用sys
模块的第一步,就是导入该模块。导入sys
模块后,我们就有了变量sys
指向该模块,利用sys
这个变量,就可以访问sys
模块的所有功能。
sys
模块有一个argv
变量,用list存储了命令行的所有参数。argv
至少有一个元素,因为第一个参数永远是该.py文件的名称
如:
运行
python3 hello.py
获得的sys.argv
就是['hello.py']
;运行
python3 hello.py Michael
获得的sys.argv
就是['hello.py', 'Michael]
。
注意到这两行代码:
1 | if __name__=='__main__': |
当我们在命令行运行hello
模块文件时,Python解释器把一个特殊变量__name__
置为__main__
,而如果在其他地方导入该hello
模块时,if
判断将失败,因此,这种if
测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。
作用域
正常的函数和变量名是公开的(public),可以被直接引用,比如:abc
,x123
,PI
等;
-
类似
__xxx__
这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__
,__name__
就是特殊变量,hello
模块定义的文档注释也可以用特殊变量__doc__
访问,我们自己的变量一般不要用这种变量名; -
类似
_xxx
和__xxx
这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc
,__abc
等;
注意:private函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。
外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public。
5.2安装第三方模块
在Python中,安装第三方模块,是通过包管理工具pip完成的。
注意:Mac或Linux上有可能并存Python 3.x和Python 2.x,因此对应的pip命令是pip3
。
一般来说,第三方库都会在Python官方的pypi.python.org网站(PyPI,Python Package Index)注册,要安装一个第三方库,必须先知道该库的名称
推荐直接使用Anaconda,这是一个基于Python的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,我们装上Anaconda,就相当于把数十个第三方模块自动安装好了,非常简单易用。
下载安装后,Anaconda会把系统Path中的python指向自己自带的Python,并且,Anaconda安装的第三方模块会安装在Anaconda自己的路径下,不影响系统已安装的Python目录。
模块搜索路径
默认情况下,Python解释器会搜索 当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys
模块的path
变量中:
1 | import sys |
如果我们要添加自己的搜索目录,有两种方法:
一是直接修改sys.path
,添加要搜索的目录:
1 | import sys |
这种方法是在运行时修改,运行结束后失效。
第二种方法是设置环境变量PYTHONPATH
,该环境变量的内容会被自动添加到模块搜索路径中。
5.3包管理工具
参考链接:花了两天,终于把 Python 的 setup.py 给整明白了、Installing Python Modules (Legacy version) — Python 3.9.5 documentation、安装 Python 模块(pip方式) — Python 3.9.5 文档
并行编译:python setuptools editable install with parallel build - Stack Overflow
1
2
3
4
5
6 python setup.py build -j 4
python setup.py install # 直接安装
python setup.py develop # 调试安装
# 以下两种调试安装未尝试
python setup.py build -j5 develop
pip install --editable . --global-option="build_ext" --global-option="-j5"
5.3.1distutils
distutils 是 python 标准库的一部分,这个库的目的是为开发者提供一种方便的打包方式, 同时为使用者提供方便的安装方式。
我们经常使用的setup.py就是基于distutils实现的,然后通过setup.py就可以进行打包或者安装了。
看一个简单的例子,找一个目录创建三个文件foo.py、bar.py和setup.py,其中setup.py的内容如下:
1 | from distutils.core import setup |
然后,在该目录中运行 python setup.py sdist
,会生成了一个"fooBar-1.0.zip"包。
使用者就可以解压缩这个包然后执行 python setup.py install
进行安装,然后就可以使用foo、bar这两个模块了
关于更过如何编写setup.py的内容,请自行参阅Python官方文档中的setupscript部分。
5.3.2setuptools 和 distribute
setuptools 是对 distutils 的增强,尤其是引入了包依赖管理。可以通过pip install setuptools
来安装setuptools。
至于distribute,它是setuptools的一个分支版本。分支的原因是有一部分开发者认为 setuptools 开发太慢。但现在,distribute 又合并回了 setuptools 中,所以可以认为它们是同一个东西。
前面看到setup.py可以创建一个压缩包,而setuptools使用了一种新的文件格式(.egg),可以为Python包创建 egg文件。setuptools 可以识别.egg文件,并解析、安装它
上述两种都会提供一个setup.py
文件,此时通过如下方式即可安装:
1 | python setup.py build |
- 在运行
install
时会提前运行build
,所以第一句可有可无
5.3.3easy_install
当安装好setuptools/distribute之后,就可以直接使用easy_install这个工具了(已经用得非常少了):
- 从PyPI上安装一个包:当使用
easy_install package
命令后,easy_install 可以自动从 PyPI 上下载相关的包,并完成安装,升级 - 下载一个包安装:通过
easy_install package.tgz
命令可以安装一个已经下载的包 - 安装egg文件:通过
easy_install package.egg
可以安装一个egg格式的文件
根据上面的分析,可以看到setuptools/distribute和easy_install之间的关系:
- setuptools/distribute 都扩展了 distutils,提供了更多的功能
- easy_install是基于setuptools/distribute的一个工具,方便了包的安装和省级
5.3.4pip
pip安装方式(一般默认都自带,无需安装):
- 通过官方手册下载get-pip.py文件,然后执行
python get-pip.py
进行安装(如果没有安装setuptools,那么get-pip.py会帮忙安装) - 下载pip源码包,然后通过setup.py进行安装
换源
临时使用:
1 | pip install -i https://pypi.tuna.tsinghua.edu.cn/simple some-package |
simple
不能少, 是https
而不是http
设为默认源:
升级 pip 到最新的版本 (>=10.0.0) 后进行配置:
1 | pip install pip -U |
如果 pip 默认源的网络连接较差,可以临时使用镜像站来升级 pip:
1 | pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U |
常用命令
使用 | 命令 |
---|---|
从PyPI安装软件包 | pip install SomePackage |
卸载软件包 | pip uninstall SomePackage |
查看以安装软件包 | pip list |
查看可升级软件包 | pip list --outdated |
升级软件包 | pip install --upgrade SomePackage |
查看软件包安装了哪些文件及路径等信息 | pip show --files SomePackage |
安装软件包的指定版本号 | pip install SomePackage # latest version pip install SomePackage==1.0.4 # specific version pip install ‘SomePackage>=1.0.4’ # minimum version |
根据依赖文件安装软件包 | pip freeze > requirements.txt # 使用pip导出依赖文件列表 pip install -r requirements.txt # 根据依赖文件列表,自动安装对应的软件包 |
六、面向对象编程
面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。
在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。
6.1类和实例
以Student类为例,在Python中,定义类是通过class
关键字:
1 | class Student(object): |
class
后面紧接着是类名,即Student
,类名通常是大写开头的单词,紧接着是(object)
,表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object
类,这是所有类最终都会继承的类。
定义好了Student
类,就可以根据Student
类创建出Student
的实例,创建实例是通过类名+()实现的:
1 | bart = Student() |
可以看到,变量bart
指向的就是一个Student
的实例,后面的0x10a67a590
是内存地址,每个object的地址都不一样,而Student
本身则是一个类。
可以自由地给一个实例变量绑定属性,比如,给实例bart
绑定一个name
属性:
1 | 'Bart Simpson' bart.name = |
由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__
方法,在创建实例的时候,就把name
,score
等属性绑上去:
1 | class Student(object): |
注意到__init__
方法的第一个参数永远是self
,表示创建的实例本身,因此,在__init__
方法内部,就可以把各种属性绑定到self
,因为self
就指向创建的实例本身。
有了__init__
方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__
方法匹配的参数,但self
不需要传,Python解释器自己会把实例变量传进去
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self
,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。
6.1.1数据封装
每个实例拥有各自的name
和score
这些数据,我们可以通过函数来访问这些数据。将这些封装数据的函数和Student
类本身关联起来,我们称之为类的方法:
1 | class Student(object): |
要定义一个方法,除了第一个参数是self
外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self
不用传递,其他参数正常传入:
1 | 'Bart Simpson', 59) bart = Student( |
封装的另一个好处是随时可以给Student
类增加新的方法
和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同:
1 | 'Bart Simpson', 59) bart = Student( |
6.2访问限制
从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的name
、score
属性
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__
,在Python中,实例的变量名如果以__
开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问
把Student类改一改:
1
2
3
4
5
6
7
8 class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def print_score(self):
print('%s: %s' % (self.__name, self.__score))改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问
实例变量.__name
和实例变量.__score
了:
如果外部代码要获取name和score,可以给Student类增加get_name
和get_score
这样的方法
同理,要允许外部代码修改score,可以再给Student类增加set_score
方法
有些时候,你会看到以一个下划线开头的实例变量名,比如_name
,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name
是因为Python解释器对外把__name
变量改成了_Student__name
,所以,仍然可以通过_Student__name
来访问__name
变量:
1 | bart._Student__name |
但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name
改成不同的变量名。
总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。
6.3继承和多态
在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)
比如,我们已经编写了一个名为
Animal
的class,有一个run()
方法可以直接打印:
1
2
3 class Animal(object):
def run(self):
print('Animal is running...')当我们需要编写
Dog
和Cat
类时,就可以直接从Animal
类继承:
1
2
3
4
5 class Dog(Animal):
pass
class Cat(Animal):
pass最大的好处是子类获得了父类的全部功能。由于
Animial
实现了run()
方法,因此,Dog
和Cat
作为它的子类,什么事也没干,就自动拥有了run()
方法也可以对子类增加一些方法,比如Dog类:
1
2
3
4
5
6
7 class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')对
Dog
和Cat
类改进如下:
1
2
3
4
5
6
7
8
9 class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')再次运行,结果如下:
1
2 Dog is running...
Cat is running...当子类和父类都存在相同的
run()
方法时,我们说,子类的run()
覆盖了父类的run()
,在代码运行的时候,总是会调用子类的run()
。这样,我们就获得了继承的另一个好处:多态。
判断一个变量是否是某个类型可以用isinstance()
判断
例子:
1
2
3
4
5
6
7
8
9 a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # c是Dog类型
isinstance(a, list)
True
isinstance(b, Animal)
True
isinstance(c, Dog)
True
多态:对于一个变量,我们只需要知道它是Animal
类型,无需确切地知道它的子类型,就可以放心地调用run()
方法,而具体调用的run()
方法是作用在Animal
、Dog
、Cat
还是Tortoise
对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal
的子类时,只要确保run()
方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
- 对扩展开放:允许新增
Animal
子类; - 对修改封闭:不需要修改依赖
Animal
类型的run_twice()
等函数。
例子:
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
28
29
30 class Animal(object):
def run(self):
print('Animal is running...')
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
#只需要animal类型的变量即可
def run_twice(animal):
animal.run()
animal.run()
#调用:
run_twice(Animal())
Animal is running...
Animal is running...
run_twice(Dog())
Dog is running...
Dog is running...
run_twice(Cat())
Cat is running...
Cat is running...
#新增Tortoise类,并调用仍然可以正常运行
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树。比如如下的继承树:
1 | ┌───────────────┐ |
6.3.1静态语言和动态语言在此处的区别
对于静态语言(例如Java)来说,如果需要传入Animal
类型,则传入的对象必须是Animal
类型或者它的子类,否则,将无法调用run()
方法。
对于Python这样的动态语言来说,则不一定需要传入Animal
类型。我们只需要保证传入的对象有一个run()
方法就可以了:
1 | class Timer(object): |
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()
方法,返回其内容。但是,许多对象,只要有read()
方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()
方法的对象。
动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。
6.4获取对象信息
type
基本类型都可以用type()
判断:
1 | type(123) |
type()
函数返回对应的Class类型
比较:
1 | #判断基本数据类型可以直接写int,str等 |
isinstance
对于class的继承关系来说,使用type()
就很不方便。我们要判断class的类型,可以使用isinstance()
函数。
例如上次的继承关系:
1 object -> Animal -> Dog -> Husky那么,
isinstance()
就可以告诉我们,一个对象是否是某种类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 a = Animal()
d = Dog()
h = Husky()
isinstance(h, Husky)
True
isinstance(h, Dog)
True
isinstance(h, Animal)
True
#基本类型
isinstance('a', str)
True
isinstance(123, int)
True
isinstance(b'a', bytes)
True
#判断一个变量是否是某些类型中的一种
isinstance([1, 2, 3], (list, tuple))
True
isinstance((1, 2, 3), (list, tuple))
True
总是优先使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。
dir
如果要获得一个对象的所有属性和方法,可以使用dir()
函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:
1 | dir('ABC') |
类似__xxx__
的属性和方法在Python中都是有特殊用途的,比如__len__
方法返回长度。在Python中,如果你调用len()
函数试图获取一个对象的长度,实际上,在len()
函数内部,它自动去调用该对象的__len__()
方法
自己写的类,如果也想用len(myObj)
的话,就自己写一个__len__()
方法:
1 | class MyDog(object): |
仅仅把属性和方法列出来是不够的,配合内置函数getattr()
、setattr()
以及hasattr()
,我们可以直接操作一个对象的状态:
1 | class MyObject(object): |
注意:只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以直接获取就直接获取
6.5实例属性和类属性
直接在class中定义属性,这种属性是类属性,归该类所有
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 class Student(object):
'Student' name =
...
# 创建实例s s = Student()
print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
print(Student.name) # 打印类的name属性
Student
'Michael' # 给实例绑定name属性 s.name =
print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
del s.name # 如果删除实例的name属性
print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student
在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
七、面向对象高级编程
7.1使用_slots_
正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。
但是,给一个实例绑定的方法,对另一个实例是不起作用的。为了给所有实例都绑定方法,可以给class绑定方法。给class绑定方法后,所有实例均可调用
如果我们想要限制实例的属性,Python允许在定义class的时候,定义一个特殊的__slots__
变量,来限制该class实例能添加的属性
例子:
1
2
3
4
5
6
7
8
9 class Student(object):
__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
# 创建新的实例 s = Student()
'Michael' # 绑定属性'name' s.name =
25 # 绑定属性'age' s.age =
99 # 绑定属性'score' s.score =
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
注意,__slots__
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的,除非在子类中也定义__slots__
,这样,子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
。
7.2使用@property
在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把属性随便改。解决方法就是增加一个set_xxx()
方法来设置属性,再通过一个get_xxx()
来获取属性,这样,在set_sss()
方法里,就可以检查参数。但是,这样的调用方法又略显复杂,没有直接用属性这么直接简单。
对于Python来说,可以利用之前的装饰器(decorator),对于类的方法,装饰器一样起作用。Python内置的@property
装饰器就是负责把一个方法变成属性调用的:
1 | class Student(object): |
@property
的实现比较复杂,我们先考察如何使用。把一个getter方法变成属性,只需要加上@property
就可以了。此时,@property
本身又创建了另一个装饰器@score.setter
,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:
1 | s = Student() |
还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:
1 | class Student(object): |
上面的birth
是可读写属性,而age
就是一个只读属性,因为age
可以根据birth
和当前时间计算出来。
7.3多重继承
继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能。
回忆一下Animal
类层次的设计,假设我们要实现以下4种动物:
- Dog - 狗狗;
- Bat - 蝙蝠;
- Parrot - 鹦鹉;
- Ostrich - 鸵鸟。
类的层次设计可以根据不同的需求来确定不同的设计,如果同时需要多种层次类型就需要多重继承。
主要的类层次仍按照哺乳类和鸟类设计:
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
28
29
30
31
32
33
34
35
36 class Animal(object):
pass
# 大类:
class Mammal(Animal):
pass
class Bird(Animal):
pass
# 各种动物:
class Dog(Mammal):
pass
class Bat(Mammal):
pass
class Parrot(Bird):
pass
class Ostrich(Bird):
pass
#给动物再加上Runnable和Flyable的功能
class Runnable(object):
def run(self):
print('Running...')
class Flyable(object):
def fly(self):
print('Flying...')
#需要Runnable功能的动物,就多继承一个Runnable
class Dog(Mammal, Runnable):
pass
#需要Flyable功能的动物,就多继承一个Flyable
class Bat(Mammal, Flyable):
pass
通过多重继承,一个子类就可以同时获得多个父类的所有功能。
7.3.1MinIn
在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich
继承自Bird
。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich
除了继承自Bird
外,再同时继承Runnable
。这种设计通常称之为MixIn。
MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。
python自带的很多库也使用了MixIn。举个例子,Python自带了
TCPServer
和UDPServer
这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn
和ThreadingMixIn
提供。通过组合,我们就可以创造出合适的服务来。比如,编写一个多进程模式的TCP服务,定义如下:
1
2
3
4
5
6
7
8
9 #多进程模式的TCP服务
class MyTCPServer(TCPServer, ForkingMixIn):
pass
#多线程模式的UDP服务
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
#协程模型
class MyTCPServer(TCPServer, CoroutineMixIn):
pass不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。
7.4定制类
除__slots__
、__len__()
之外,Python的class中还有许多这种形如__xxx__
的变量或者函数名,可以帮助我们定制类。
7.4.1__str__、_repr_
我们先定义一个Student
类,打印一个实例:
1 | class Student(object): |
打印出一堆<__main__.Student object at 0x109afb190>
,不好看。
怎么才能打印得好看呢?只需要定义好__str__()
方法,返回一个好看的字符串就可以了:
1 | class Student(object): |
这样打印出来的实例,不但好看,而且容易看出实例内部重要的数据。
但是细心的朋友会发现直接敲变量不用print
,打印出来的实例还是不好看:
1 | 'Michael') s = Student( |
这是因为直接显示变量调用的不是__str__()
,而是__repr__()
,两者的区别是__str__()
返回用户看到的字符串,而__repr__()
返回程序开发者看到的字符串,也就是说,__repr__()
是为调试服务的。
解决办法是再定义一个__repr__()
。但是通常__str__()
和__repr__()
代码都是一样的,所以,有个偷懒的写法:
1 | class Student(object): |
7.4.2_iter_
如果一个类想被用于for ... in
循环,类似list或tuple那样,就必须实现一个__iter__()
方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()
方法拿到循环的下一个值,直到遇到StopIteration
错误时退出循环。
我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:
1 | class Fib(object): |
现在,试试把Fib实例作用于for循环:
1 | for n in Fib(): |
7.4.3_getitem_
Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:
1 | 5] Fib()[ |
要表现得像list那样按照下标取出元素,需要实现__getitem__()
方法:
1 | class Fib(object): |
现在,就可以按下标访问数列的任意一项了:
1 | f = Fib() |
但是list有个神奇的切片方法:
1 | list(range(100))[5:10] |
对于Fib却报错。原因是__getitem__()
传入的参数可能是一个int,也可能是一个切片对象slice
,所以要做判断:
1 | class Fib(object): |
现在试试Fib的切片:
1 | f = Fib() |
但是没有对step参数作处理:
1 | 10:2] f[: |
也没有对负数作处理,所以,要正确实现一个__getitem__()
还是有很多工作要做的。
此外,如果把对象看成dict
,__getitem__()
的参数也可能是一个可以作key的object,例如str
。
与之对应的是__setitem__()
方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()
方法,用于删除某个元素。
总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
7.4.4_getattr_
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。比如定义Student
类:
1 | class Student(object): |
调用name
属性,没问题,但是,调用不存在的score
属性,就有问题了:
1 | s = Student() |
错误信息很清楚地告诉我们,没有找到score
这个attribute。
要避免这个错误,除了可以加上一个score
属性外,Python还有另一个机制,那就是写一个__getattr__()
方法,动态返回一个属性。修改如下:
1 | class Student(object): |
当调用不存在的属性时,比如score
,Python解释器会试图调用__getattr__(self, 'score')
来尝试获得属性,这样,我们就有机会返回score
的值:
1 | s = Student() |
返回函数也是完全可以的:
1 | class Student(object): |
只是调用方式要变为:
1 | s.age() |
注意,只有在没有找到属性的情况下,才调用__getattr__
,已有的属性,比如name
,不会在__getattr__
中查找。
此外,注意到任意调用如s.abc
都会返回None
,这是因为我们定义的__getattr__
默认返回就是None
。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError
的错误:
1 | class Student(object): |
这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。
这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。
举个例子:
现在很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:
如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。
利用完全动态的
__getattr__
,我们可以写出一个链式调用:
1
2
3
4
5
6
7
8
9
10
11
12 class Chain(object):
def __init__(self, path=''):
self._path = path
def __getattr__(self, path):
return Chain('%s/%s' % (self._path, path))
def __str__(self):
return self._path
__repr__ = __str__试试:
1
2 list Chain().status.user.timeline.
'/status/user/timeline/list'这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用,而且,不随API的增加而改变!
还有些REST API会把参数放到URL中,比如GitHub的API:
1 GET /users/:user/repos调用时,需要把
:user
替换为实际用户名。如果我们能写出这样的链式调用:
1 Chain().users('michael').repos就可以非常方便地调用API了。有兴趣的童鞋可以试试写出来。
7.4.5_call_
一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()
来调用。能不能直接在实例本身上调用呢?在Python中,答案是肯定的。
任何类,只需要定义一个__call__()
方法,就可以直接对实例进行调用。请看示例:
1 | class Student(object): |
调用方式如下:
1 | 'Michael') s = Student( |
__call__()
还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。
如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。
那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable
对象,比如函数和我们上面定义的带有__call__()
的类实例:
1 | callable(Student()) |
通过callable()
函数,我们就可以判断一个对象是否是“可调用”对象。
7.5使用枚举类
当我们需要定义常量时,一个办法是用大写变量通过整数来定义,好处是简单,缺点是类型是int
,并且仍然是变量。
更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum
类来实现这个功能:
1 | from enum import Enum |
这样我们就获得了Month
类型的枚举类,可以直接使用Month.Jan
来引用一个常量,或者枚举它的所有成员:
1 | for name, member in Month.__members__.items(): |
value
属性则是自动赋给成员的int
常量,默认从1
开始计数。
如果需要更精确地控制枚举类型,可以从Enum
派生出自定义类:
1 | from enum import Enum, unique |
@unique
装饰器可以帮助我们检查保证没有重复值。
访问这些枚举类型可以有若干种方法:
1 | day1 = Weekday.Mon |
可见,既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量。
7.6使用元类
7.6.1type()
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
比方说我们要定义一个Hello
的class,就写一个hello.py
模块:
1 | class Hello(object): |
当Python解释器载入hello
模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello
的class对象,测试如下:
1 | from hello import Hello |
type()
函数可以查看一个类型或变量的类型,Hello
是一个class,它的类型就是type
,而h
是一个实例,它的类型就是class Hello
。
我们说class的定义是运行时动态创建的,而创建class的方法就是使用type()
函数。
type()
函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()
函数创建出Hello
类,而无需通过class Hello(object)...
的定义:
1 | def fn(self, name='world'): # 先定义函数 |
要创建一个class对象,type()
函数依次传入3个参数:
- class的名称;
- 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
- class的方法名称与函数绑定,这里我们把函数
fn
绑定到方法名hello
上。
通过type()
函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()
函数创建出class。
正常情况下,我们都用class Xxx...
来定义类,但是,type()
函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。
7.6.2metaclass
除了使用type()
动态创建类以外,要控制类的创建行为,还可以使用metaclass。
metaclass,直译为元类,简单的解释就是:
当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。
连接起来就是:先定义metaclass,就可以创建类,最后创建实例。
所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。
metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。
我们先看一个简单的例子,这个metaclass可以给我们自定义的MyList增加一个add
方法:
定义ListMetaclass
,按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass:
1 | # metaclass是类的模板,所以必须从`type`类型派生: |
有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass
:
1 | class MyList(list, metaclass=ListMetaclass): |
当我们传入关键字参数metaclass
时,魔术就生效了,它指示Python解释器在创建MyList
时,要通过ListMetaclass.__new__()
来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。
__new__()
方法接收到的参数依次是:
- 当前准备创建的类的对象;
- 类的名字;
- 类继承的父类集合;
- 类的方法集合。
测试一下MyList
是否可以调用add()
方法:
1 | L = MyList() |
而普通的list
没有add()
方法:
1 | list() L2 = |
动态修改有什么意义?直接在MyList
定义中写上add()
方法不是更简单吗?正常情况下,确实应该直接写,通过metaclass修改纯属变态。
但是,总会遇到需要通过metaclass修改类定义的。ORM就是一个典型的例子。
ORM全称“Object Relational Mapping”,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。
要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
让我们来尝试编写一个ORM框架。
编写底层模块的第一步,就是先把调用接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User
类来操作对应的数据库表User
,我们期待他写出这样的代码:
1 | class User(Model): |
其中,父类Model
和属性类型StringField
、IntegerField
是由ORM框架提供的,剩下的魔术方法比如save()
全部由metaclass自动完成。虽然metaclass的编写会比较复杂,但ORM的使用者用起来却异常简单。
现在,我们就按上面的接口来实现该ORM。
首先来定义Field
类,它负责保存数据库表的字段名和字段类型:
1 | class Field(object): |
在Field
的基础上,进一步定义各种类型的Field
,比如StringField
,IntegerField
等等:
1 | class StringField(Field): |
下一步,就是编写最复杂的ModelMetaclass
了:
1 | class ModelMetaclass(type): |
以及基类Model
:
1 | class Model(dict, metaclass=ModelMetaclass): |
当用户定义一个class User(Model)
时,Python解释器首先在当前类User
的定义中查找metaclass
,如果没有找到,就继续在父类Model
中查找metaclass
,找到了,就使用Model
中定义的metaclass
的ModelMetaclass
来创建User
类,也就是说,metaclass可以隐式地继承到子类,但子类自己却感觉不到。
在ModelMetaclass
中,一共做了几件事情:
- 排除掉对
Model
类的修改; - 在当前类(比如
User
)中查找定义的类的所有属性,如果找到一个Field属性,就把它保存到一个__mappings__
的dict中,同时从类属性中删除该Field属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性); - 把表名保存到
__table__
中,这里简化为表名默认为类名。
在Model
类中,就可以定义各种操作数据库的方法,比如save()
,delete()
,find()
,update
等等。
我们实现了save()
方法,把一个实例保存到数据库中。因为有表名,属性到字段的映射和属性值的集合,就可以构造出INSERT
语句。
编写代码试试:
1 | u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd') |
输出如下:
1 | Found model: User |
可以看到,save()
方法已经打印出了可执行的SQL语句,以及参数列表,只需要真正连接到数据库,执行该SQL语句,就可以完成真正的功能。
metaclass是Python中非常具有魔术性的对象,它可以改变类创建时的行为。这种强大的功能使用起来务必小心。
八、错误、调试和测试
8.1错误处理
8.1.1try
高级语言通常都内置了一套try...except...finally...
的错误处理机制,Python也不例外。
1 | try: |
由于没有错误发生,所以except
语句块不会被执行,但是finally
如果有,则一定会被执行(可以没有finally
语句)。
此外,如果没有错误发生,可以在except
语句块后面加一个else
,当没有错误发生时,会自动执行else
语句:
1 | try: |
Python的错误其实也是class,所有的错误类型都继承自BaseException
,所以在使用except
时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”
Python所有的错误都是从BaseException
类派生的,常见的错误类型和继承关系看这里:
https://docs.python.org/zh-cn/3/library/exceptions.html#exception-hierarchy
使用try...except
捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()
调用foo()
,foo()
调用bar()
,结果bar()
出错了,这时,只要main()
捕获到了,就可以处理。也就是说,不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。
8.1.2调用栈
如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 # err.py文件:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
bar('0')
main()
#输出
$ python3 err.py
Traceback (most recent call last):#告诉我们这是错误的跟踪信息。
File "err.py", line 11, in <module>#调用main()出错了,在代码文件err.py的第11行代码
main()
File "err.py", line 9, in main#调用bar('0')出错了,在代码文件err.py的第9行代码
bar('0')
File "err.py", line 6, in bar#原因是return foo(s) * 2这个语句出错了,但这还不是最终原因
return foo(s) * 2
File "err.py", line 3, in foo#原因是return 10 / int(s)这个语句出错了
return 10 / int(s)
ZeroDivisionError: division by zero#表明这是错误产生的源头出错的时候,一定要分析错误的调用栈信息,才能定位错误的位置。
8.1.3记录错误
如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。
Python内置的logging
模块可以非常容易地记录错误信息:
1 | # err_logging.py |
同样是出错,但程序打印完错误信息后会继续执行,并正常退出:
1 | $ python3 err_logging.py |
通过配置,logging
还可以把错误记录到日志文件里,方便事后排查。
8.1.4抛出错误
因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。
如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise
语句抛出一个错误的实例:
1 | # err_raise.py |
只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueError
,TypeError
),尽量使用Python内置的错误类型。
最后,我们来看另一种错误处理的方式:
1 | # err_reraise.py |
在bar()
函数中,已经捕获了错误,但是,打印一个ValueError!
后,又把错误通过raise
语句抛出去了。捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。
raise
语句如果不带参数,就会把当前错误原样抛出。此外,在except
中raise
一个Error,还可以把一种类型的错误转化成另一种类型:
1 | try: |
只要是合理的转换逻辑就可以,但是,决不应该把一个IOError
转换成毫不相干的ValueError
。
8.2调试
第一种方法简单直接粗暴有效,就是用print()
把可能有问题的变量打印出来看看
8.2.1断言
第二种是用断言。凡是用print()
来辅助查看的地方,都可以用断言(assert)来替代
1 | def foo(s): |
assert
的意思是,表达式n != 0
应该是True
,否则,根据程序运行的逻辑,后面的代码肯定会出错。
如果断言失败,assert
语句本身就会抛出AssertionError
:
1 | $ python err.py |
程序中如果到处充斥着assert
,和print()
相比也好不到哪去。不过,启动Python解释器时可以用-O
参数来关闭assert
:
1 | $ python -O err.py |
8.2.2logging
把print()
替换为logging
是第3种方式,和assert
比,logging
不会抛出错误,而且可以输出到文件:
1 | import logging |
logging.info()
就可以输出一段文本。运行,发现除了ZeroDivisionError
,没有任何信息。怎么回事?
别急,在import logging
之后添加一行配置再试试:
1 | import logging |
看到输出了:
1 | $ python err.py |
这就是logging
的好处,它允许你指定记录信息的级别,有debug
,info
,warning
,error
等几个级别,当我们指定level=INFO
时,logging.debug
就不起作用了。同理,指定level=WARNING
后,debug
和info
就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging
的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。
8.2.3pdb
第4种方式是启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。我们先准备好程序:
1 | # err.py |
然后启动:
1 | $ python -m pdb err.py |
以参数-m pdb
启动后,pdb定位到下一步要执行的代码-> s = '0'
。输入命令l
来查看代码:
1 | (Pdb) l |
输入命令n
可以单步执行代码:
1 | (Pdb) n |
任何时候都可以输入命令p 变量名
来查看变量:
1 | (Pdb) p s |
输入命令q
结束调试,退出程序:
1 | (Pdb) q |
这种通过pdb在命令行调试的方法理论上是万能的,但实在是太麻烦了,如果有一千行代码,要运行到第999行得敲多少命令啊。还好,我们还有另一种调试方法。
1 | pdb.set_trace() |
这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb
,然后,在可能出错的地方放一个pdb.set_trace()
,就可以设置一个断点:
1 | # err.py |
运行代码,程序会自动在pdb.set_trace()
暂停并进入pdb调试环境,可以用命令p
查看变量,或者用命令c
继续运行:
1 | $ python err.py |
这个方式比直接启动pdb单步调试效率要高很多,但也高不到哪去。
8.3单元测试
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对函数abs()
,我们可以编写出以下几个测试用例:
- 输入正数,比如
1
、1.2
、0.99
,期待返回值与输入相同; - 输入负数,比如
-1
、-1.2
、-0.99
,期待返回值与输入相反; - 输入
0
,期待返回0
; - 输入非数值类型,比如
None
、[]
、{}
,期待抛出TypeError
。
把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。
如果单元测试通过,说明我们测试的这个函数能够正常工作。如果单元测试不通过,要么函数有bug,要么测试条件输入不正确,总之,需要修复使单元测试能够通过。
单元测试通过后有什么意义呢?如果我们对abs()
函数代码做了修改,只需要再跑一遍单元测试,如果通过,说明我们的修改不会对abs()
函数原有的行为造成影响,如果测试不通过,说明我们的修改与原有行为不一致,要么修改代码,要么修改测试。
这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
我们来编写一个Dict
类,这个类的行为和dict
一致,但是可以通过属性来访问,用起来就像下面这样:
1 | Dict(a=1, b=2) d = |
mydict.py
代码如下:
1 | class Dict(dict): |
为了编写单元测试,我们需要引入Python自带的unittest
模块,编写mydict_test.py
如下:
1 | import unittest |
编写单元测试时,我们需要编写一个测试类,从unittest.TestCase
继承。
以test
开头的方法就是测试方法,不以test
开头的方法不被认为是测试方法,测试的时候不会被执行。
对每一类测试都需要编写一个test_xxx()
方法。由于unittest.TestCase
提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual()
:
1 | self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等 |
另一种重要的断言就是期待抛出指定类型的Error,比如通过d['empty']
访问不存在的key时,断言会抛出KeyError
:
1 | with self.assertRaises(KeyError): |
而通过d.empty
访问不存在的key时,我们期待抛出AttributeError
:
1 | with self.assertRaises(AttributeError): |
8.3.1运行单元测试
一旦编写好单元测试,我们就可以运行单元测试。最简单的运行方式是在mydict_test.py
的最后加上两行代码:
1 | if __name__ == '__main__': |
这样就可以把mydict_test.py
当做正常的python脚本运行:
1 | $ python mydict_test.py |
另一种方法是在命令行通过参数-m unittest
直接运行单元测试:
1 | $ python -m unittest mydict_test |
这是推荐的做法,因为这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试。
8.3.2setUp与tearDown
可以在单元测试中编写两个特殊的setUp()
和tearDown()
方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
setUp()
和tearDown()
方法有什么用呢?设想你的测试需要启动一个数据库,这时,就可以在setUp()
方法中连接数据库,在tearDown()
方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:
1 | class TestDict(unittest.TestCase): |
可以再次运行测试看看每个测试方法调用前后是否会打印出setUp...
和tearDown...
。
8.3.3文档测试
如果你经常阅读Python的官方文档,可以看到很多文档都有示例代码。比如re模块就带了很多示例代码:
1 | import re |
可以把这些示例代码在Python的交互式环境下输入并执行,结果与文档中的示例代码显示的一致。
这些代码与其他说明可以写在注释中,然后,由一些工具来自动生成文档。既然这些代码本身就可以粘贴出来直接运行,那么,可不可以自动执行写在注释中的这些代码呢?
答案是肯定的。
当我们编写注释时,如果写上这样的注释:
1 | def abs(n): |
无疑更明确地告诉函数的调用者该函数的期望输入和输出。
并且,Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用...
表示中间一大段烦人的输出。
让我们用doctest来测试上次编写的Dict
类:
1 | # mydict2.py |
运行python mydict2.py
:
1 | $ python mydict2.py |
什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题,比如把__getattr__()
方法注释掉,再运行就会报错:
1 | $ python mydict2.py |
注意到最后3行代码。当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。
九、IO编程
举个例子来说,比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒,怎么办呢?有两种办法:
第一种是CPU等着,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO;
另一种方法是CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以立刻接着执行,这种模式称为异步IO。
同步和异步的区别就在于是否等待IO执行的结果。很明显,使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。
操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。我们后面会详细讨论Python的IO编程接口。
注意,本章的IO编程都是同步模式,异步IO由于复杂度太高,后续涉及到服务器端程序开发时我们再讨论。
其他未提到的情况可以参考:Python文件操作,看这篇就足够
9.1文件读写
Python内置了读写文件的函数,用法和C是兼容的。
9.1.1读文件
要以读文件的模式打开一个文件对象,使用Python内置的open()
函数,传入文件名和标示符
1 | open('/Users/michael/test.txt', 'r')#默认读取UTF-8编码的文本文件 f = |
如果文件不存在,
open()
函数就会抛出一个IOError
的错误,并且给出错误码和详细的信息告诉你文件不存在:
1
2
3
4 open('/Users/michael/notfound.txt', 'r') f=
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '/Users/michael/notfound.txt'
要读取二进制文件,比如图片、视频等等,用
'rb'
模式打开文件即可:
1
2
3 open('/Users/michael/test.jpg', 'rb') f =
f.read()
b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六进制表示的字节读取UTF-8编码的文本文件:
1
2
3 open('/Users/michael/gbk.txt', 'r', encoding='gbk') f =
f.read()
'测试'遇到有些编码不规范的文件,你可能会遇到
UnicodeDecodeError
,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()
函数还接收一个errors
参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略:
1 open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore') f =
如果文件打开成功,接下来,调用read()
方法可以一次读取文件的全部内容,Python把内容读到内存,用一个str
对象表示:
1 | f.read() |
最后一步是调用close()
方法关闭文件。文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的:
1 | f.close() |
由于文件读写时都有可能产生
IOError
,一旦出错,后面的f.close()
就不会调用。所以,为了保证无论是否出错都能正确地关闭文件,我们可以使用try ... finally
来实现:
1
2
3
4
5
6 try:
f = open('/path/to/file', 'r')
print(f.read())
finally:
if f:
f.close()
但是每次都这么写实在太繁琐,所以,Python引入了with
语句来自动帮我们调用close()
方法:
1 | with open('/path/to/file', 'r') as f: |
这和前面的try ... finally
是一样的,但是代码更佳简洁,并且不必调用f.close()
方法。
调用read()
会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,可以反复调用read(size)
方法,每次最多读取size个字节的内容。另外,调用readline()
可以每次读取一行内容,调用readlines()
一次读取所有内容并按行返回list
。因此,要根据需要决定怎么调用。
如果文件很小,read()
一次性读取最方便;如果不能确定文件大小,反复调用read(size)
比较保险;如果是配置文件,调用readlines()
最方便:
1 | for line in f.readlines(): |
9.1.2file-like object
像open()
函数返回的这种有个read()
方法的对象,在Python中统称为file-like Object。除了file外,还可以是内存的字节流,网络流,自定义流等等。file-like Object不要求从特定类继承,只要写个read()
方法就行。
StringIO
就是在内存中创建的file-like Object,常用作临时缓冲。
9.1.3写文件
写文件和读文件是一样的,唯一区别是调用open()
函数时,传入标识符'w'
或者'wb'
表示写文本文件或写二进制文件:
1 | open('/Users/michael/test.txt', 'w') f = |
你可以反复调用write()
来写入文件,但是务必要调用f.close()
来关闭文件。当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用close()
方法时,操作系统才保证把没有写入的数据全部写入磁盘。忘记调用close()
的后果是数据可能只写了一部分到磁盘,剩下的丢失了。所以,还是用with
语句来得保险:
1 | with open('/Users/michael/test.txt', 'w') as f: |
要写入特定编码的文本文件,请给open()
函数传入encoding
参数,将字符串自动转换成指定编码。
细心的童鞋会发现,以'w'
模式写入文件时,如果文件已存在,会直接覆盖(相当于删掉后新写入一个文件)。如果我们希望追加到文件末尾怎么办?可以传入'a'
以追加(append)模式写入。
所有模式的定义及含义可以参考Python内建函数的官方文档。
9.2StringIO和BytesIO
9.2.1StringIO
StringIO顾名思义就是在内存中读写str。
要把str写入StringIO,我们需要先创建一个StringIO,然后,像文件一样写入即可:
1 | from io import StringIO |
getvalue()
方法用于获得写入后的str。
要读取StringIO,可以用一个str初始化StringIO,然后,像读文件一样读取:
1 | from io import StringIO |
9.2.2BytesIO
StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。
BytesIO实现了在内存中读写bytes,我们创建一个BytesIO,然后写入一些bytes:
1 | from io import BytesIO |
请注意,写入的不是str,而是经过UTF-8编码的bytes。
和StringIO类似,可以用一个bytes初始化BytesIO,然后,像读文件一样读取:
1 | from io import BytesIO |
StringIO和BytesIO是在内存中操作str和bytes的方法,使得和读写文件具有一致的接口。
9.3操作文件和目录
Python内置的os
模块也可以直接调用操作系统提供的接口函数:
1 | import os |
如果是posix
,说明系统是Linux
、Unix
或Mac OS X
,如果是nt
,就是Windows
系统。
要获取详细的系统信息,可以调用uname()
函数:
1 | os.uname() |
注意uname()
函数在Windows上不提供,也就是说,os
模块的某些函数是跟操作系统相关的。
9.3.1环境变量
在操作系统中定义的环境变量,全部保存在os.environ
这个变量中,可以直接查看:
1 | os.environ |
要获取某个环境变量的值,可以调用os.environ.get('key')
:
1 | 'PATH') os.environ.get( |
9.3.2操作文件和目录
操作文件和目录的函数一部分放在os
模块中,一部分放在os.path
模块中,这一点要注意一下。查看、创建和删除目录可以这么调用:
1 | # 查看当前目录的绝对路径: |
把两个路径合成一个时,不要直接拼字符串,而要通过os.path.join()
函数,这样可以正确处理不同操作系统的路径分隔符。在Linux/Unix/Mac下,os.path.join()
返回这样的字符串:
1 | part-1/part-2 |
而Windows下会返回这样的字符串:
1 | part-1\part-2 |
同样的道理,要拆分路径时,也不要直接去拆字符串,而要通过os.path.split()
函数,这样可以把一个路径拆分为两部分,后一部分总是最后级别的目录或文件名:
1 | '/Users/michael/testdir/file.txt') os.path.split( |
os.path.splitext()
可以直接让你得到文件扩展名,很多时候非常方便:
1 | '/path/to/file.txt') os.path.splitext( |
这些合并、拆分路径的函数并不要求目录和文件要真实存在,它们只对字符串进行操作。
文件操作使用下面的函数。假定当前目录下有一个test.txt
文件:
1 | # 对文件重命名: |
但是复制文件的函数居然在os
模块中不存在!原因是复制文件并非由操作系统提供的系统调用。理论上讲,我们通过上一节的读写文件可以完成文件复制,只不过要多写很多代码。
幸运的是shutil
模块提供了copyfile()
的函数,你还可以在shutil
模块中找到很多实用函数,它们可以看做是os
模块的补充。
最后看看如何利用Python的特性来过滤文件。比如我们要列出当前目录下的所有目录,只需要一行代码:
1 | for x in os.listdir('.') if os.path.isdir(x)] [x |
要列出所有的.py
文件,也只需一行代码:
1 | for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py'] [x |
9.4序列化
9.4.1pickle
在程序运行的过程中,所有的变量都是在内存中,
我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。
序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。
反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。
Python提供了pickle
模块来实现序列化。
pickle.dumps()
方法把任意对象序列化成一个bytes
,然后,就可以把这个bytes
写入文件。或者用另一个方法pickle.dump()
直接把对象序列化后写入一个file-like Object
1 | import pickle |
当我们要把对象从磁盘读到内存时,可以先把内容读到一个bytes
,然后用pickle.loads()
方法反序列化出对象,也可以直接用pickle.load()
方法从一个file-like Object
中直接反序列化出对象。
1 | open('dump.txt', 'rb') f = |
Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据,不能成功地反序列化也没关系。
9.4.2JSON
Python的dict
对象可以直接序列化为JSON的{}
,不过,很多时候,我们更喜欢用class
表示对象,比如定义Student
类,然后序列化:
1 | import json |
运行代码,毫不留情地得到一个TypeError
:
1 | Traceback (most recent call last): |
错误的原因是Student
对象不是一个可序列化为JSON的对象。
如果连class
的实例对象都无法序列化为JSON,这肯定不合理!
别急,我们仔细看看dumps()
方法的参数列表,可以发现,除了第一个必须的obj
参数外,dumps()
方法还提供了一大堆的可选参数:
https://docs.python.org/zh-cn/3/library/json.html#json.dumps
这些可选参数就是让我们来定制JSON序列化。前面的代码之所以无法把Student
类实例序列化为JSON,是因为默认情况下,dumps()
方法不知道如何将Student
实例变为一个JSON的{}
对象。
可选参数default
就是把任意一个对象变成一个可序列为JSON的对象,我们只需要为Student
专门写一个转换函数,再把函数传进去即可:
1 | def student2dict(std): |
这样,Student
实例首先被student2dict()
函数转换成dict
,然后再被顺利序列化为JSON:
1 | print(json.dumps(s, default=student2dict)) |
不过,下次如果遇到一个Teacher
类的实例,照样无法序列化为JSON。我们可以偷个懒,把任意class
的实例变为dict
:
1 | print(json.dumps(s, default=lambda obj: obj.__dict__)) |
因为通常class
的实例都有一个__dict__
属性,它就是一个dict
,用来存储实例变量。也有少数例外,比如定义了__slots__
的class。
同样的道理,如果我们要把JSON反序列化为一个Student
对象实例,loads()
方法首先转换出一个dict
对象,然后,我们传入的object_hook
函数负责把dict
转换为Student
实例:
1 | def dict2student(d): |
运行结果如下:
1 | '{"age": 20, "score": 88, "name": "Bob"}' json_str = |
打印出的是反序列化的Student
实例对象。
对中文进行JSON序列化时,json.dumps()
提供了一个ensure_ascii
参数,当ensure_ascii=True
时,会将中文转化为unicode编码,反之ensure_ascii=False
,仍转化为汉字
十、进程和线程
10.1多进程
Python的os
模块封装了常见的系统调用,其中就包括fork
,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()
调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0
,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()
就可以拿到父进程的ID。
注意:由于Windows没有fork
调用,上面的代码在Windows上无法运行。
10.1.1multiprocessing
multiprocessing
模块是跨平台版本的多进程模块。
multiprocessing
模块提供了一个Process
类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:
1 | from multiprocessing import Process |
执行结果如下:
1 | Parent process 928. |
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process
实例,用start()
方法启动,这样创建进程比fork()
还要简单。
join()
方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
10.1.2pool
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
1 | from multiprocessing import Pool |
执行结果如下:
1 | Parent process 669. |
代码解读:
对Pool
对象调用join()
方法会等待所有子进程执行完毕,调用join()
之前必须先调用close()
,调用close()
之后就不能继续添加新的Process
了。
请注意输出的结果,task 0
,1
,2
,3
是立刻执行的,而task 4
要等待前面某个task完成后才执行,这是因为Pool
的默认大小在我的电脑上是4,因此,最多同时执行4个进程。这是Pool
有意设计的限制,并不是操作系统的限制。如果改成:
1 | p = Pool(5) |
就可以同时跑5个进程。
由于Pool
的默认大小是CPU的核数,如果你不幸拥有8核CPU,你要提交至少9个子进程才能看到上面的等待效果。
10.1.3子进程
很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。
subprocess
模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
下面的例子演示了如何在Python代码中运行命令nslookup www.python.org
,这和命令行直接运行的效果是一样的:
1 | import subprocess |
运行结果:
1 | $ nslookup www.python.org |
如果子进程还需要输入,则可以通过communicate()
方法输入:
1 | import subprocess |
上面的代码相当于在命令行执行命令nslookup
,然后手动输入:
1 | set q=mx |
运行结果如下:
1 | $ nslookup |
10.1.4进程间通信
Process
之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing
模块包装了底层的机制,提供了Queue
、Pipes
等多种方式来交换数据。
我们以Queue
为例,在父进程中创建两个子进程,一个往Queue
里写数据,一个从Queue
里读数据:
1 | from multiprocessing import Process, Queue |
运行结果如下:
1 | Process to write: 50563 |
在Unix/Linux下,multiprocessing
模块封装了fork()
调用,使我们不需要关注fork()
的细节。由于Windows没有fork
调用,因此,multiprocessing
需要“模拟”出fork
的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去,所以,如果multiprocessing
在Windows下调用失败了,要先考虑是不是pickle失败了。
10.2多线程
Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_thread
和threading
,_thread
是低级模块,threading
是高级模块,对_thread
进行了封装。绝大多数情况下,我们只需要使用threading
这个高级模块。
启动一个线程就是把一个函数传入并创建Thread
实例,然后调用start()
开始执行:
1 | import time, threading |
执行结果如下:
1 | thread MainThread is running... |
由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading
模块有个current_thread()
函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread
,子线程的名字在创建时指定,我们用LoopThread
命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1
,Thread-2
……
10.2.1Lock
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
此时需要创建一把锁,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()
来实现:
1 | balance = 0 |
当多个线程同时执行lock.acquire()
时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。
获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally
来确保锁一定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
10.2.2多核CPU
如果你不幸拥有一个多核CPU,你肯定在想,多核应该可以同时执行多个线程。
假设想把N核CPU的核心全部跑满,就必须启动N个死循环线程。
试试用Python写个死循环:
1 | import threading, multiprocessing |
启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。
但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。
不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。
10.3ThreadLocal
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:
1 | def process_student(name): |
每个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,因为每个线程处理不同的Student
对象,不能共享。
如果用一个全局dict
存放所有的Student
对象,然后以thread
自身作为key
获得线程对应的Student
对象如何?
1 | global_dict = {} |
这种方式理论上是可行的,它最大的优点是消除了std
对象在每层函数中的传递问题,但是,每个函数获取std
的代码有点丑。
有没有更简单的方式?
ThreadLocal
应运而生,不用查找dict
,ThreadLocal
帮你自动做这件事:
1 | import threading |
执行结果:
1 | Hello, Alice (in Thread-A) |
全局变量local_school
就是一个ThreadLocal
对象,每个Thread
对它都可以读写student
属性,但互不影响。你可以把local_school
看成全局变量,但每个属性如local_school.student
都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal
内部会处理。
可以理解为全局变量local_school
是一个dict
,不但可以用local_school.student
,还可以绑定其他变量,如local_school.teacher
等等。
ThreadLocal
最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
一个ThreadLocal
变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal
解决了参数在一个线程中各个函数之间互相传递的问题。
10.4分布式进程
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing
模块不但支持多进程,其中managers
子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers
模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。
举个例子:如果我们已经有一个通过Queue
通信的多进程程序在同一台机器上运行,现在,由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?
原有的Queue
可以继续使用,但是,通过managers
模块把Queue
通过网络暴露出去,就可以让其他机器的进程访问Queue
了。
我们先看服务进程,服务进程负责启动Queue
,把Queue
注册到网络上,然后往Queue
里面写入任务:
1 | # task_master.py |
windows下绑定调用接口不能使用lambda,所以只能先定义函数再绑定,其次,在绑定端口并设置验证码中,windows下需要填写ip地址,linux下不填默认为本地:
1 | def gettask(): |
请注意,当我们在一台机器上写多进程程序时,创建的Queue
可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue
不可以直接对原始的task_queue
进行操作,那样就绕过了QueueManager
的封装,必须通过manager.get_task_queue()
获得的Queue
接口添加。
然后,在另一台机器上启动任务进程(本机上启动也可以):
1 | # task_worker.py |
任务进程要通过网络连接到服务进程,所以要指定服务进程的IP。
现在,可以试试分布式进程的工作效果了。先启动task_master.py
服务进程:
1 | $ python3 task_master.py |
task_master.py
进程发送完任务后,开始等待result
队列的结果。现在启动task_worker.py
进程:
1 | $ python3 task_worker.py |
task_worker.py
进程结束,在task_master.py
进程中会继续打印出结果:
1 | Result: 3411 * 3411 = 11634921 |
这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker,就可以把任务分布到几台甚至几十台机器上,比如把计算n*n
的代码换成发送邮件,就实现了邮件队列的异步发送。
Queue对象存储在哪?注意到task_worker.py
中根本没有创建Queue的代码,所以,Queue对象存储在task_master.py
进程中:
1 | │ |
而Queue
之所以能通过网络访问,就是通过QueueManager
实现的。由于QueueManager
管理的不止一个Queue
,所以,要给每个Queue
的网络调用接口起个名字,比如get_task_queue
。
authkey
有什么用?这是为了保证两台机器正常通信,不被其他机器恶意干扰。如果task_worker.py
的authkey
和task_master.py
的authkey
不一致,肯定连接不上。
Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。
注意Queue的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。
十二、正则表达式
- 通用的字符串表达框架
- 用来简洁的表达一组字符串的表达式
- 是字符串表达”简洁“和”特征“思想的工具
- 判断某字符串的特征归属
正则表达式是由字符和操作符构成的
操作符:
操作符 | 说明 | 实例 |
---|---|---|
. | 表示任何单个字符 | |
[] | 字符集,对单个字符给出取值范围 | [abc]表示a、b、c,[a-z]表示a到z单个字符 |
[^] | 非字符集,对单个字符给出排除范围 | [^abc]表示非a或b或c的单个字符 |
* | 前一个字符0次或无限次扩展 | abc*表示ab、abc、abcc、abccc等 |
+ | 前一个字符1次或无限次扩展 | abc+表示abc、abcc、abccc等 |
? | 前一个字符0次或1次扩展 | abc?表示abc、abcc、abccc等 |
| | 左右表达式任意一个 | abc|def表示abc或者是def |
{m} | 扩展前一个字符m次 | ab{2}c表示abbc |
{m,n} | 扩展前一个字符m至n次(包含n) | ab{1,2}c表示abc、abbc |
^ | 匹配字符串开头 | ^abc表示abc且在一个字符串的开头 |
$ | 匹配字符串结尾 | abc$表示abc且在一个字符串的结尾 |
() | 分组标记,内部只能使用|操作符 | (abc)表示abc,(abc|def)表示abc或def |
\d | 数字,等价于[0-9] | |
\w | 单词字符,等价于[A-Za-z0-9_] |
12.1基础
在正则表达式中,如果直接给出字符,就是精确匹配。用\d
可以匹配一个数字,\w
可以匹配一个字母或数字,\W
可一匹配一个非字母或数字的字符,所以:
'00\d'
可以匹配'007'
,但无法匹配'00A'
;'\d\d\d'
可以匹配'010'
;'\w\w\d'
可以匹配'py3'
;
.
可以匹配任意字符,所以:
'py.'
可以匹配'pyc'
、'pyo'
、'py!'
等等。
要匹配变长的字符,在正则表达式中,用*
表示任意个字符(包括0个),用+
表示至少一个字符,用?
表示0个或1个字符,用{n}
表示n个字符,用{n,m}
表示n-m个字符:
来看一个复杂的例子:\d{3}\s+\d{3,8}
。
我们来从左到右解读一下:
\d{3}
表示匹配3个数字,例如'010'
;\s
可以匹配一个空格(也包括Tab等空白符),所以\s+
表示至少有一个空格,例如匹配' '
,' '
等;\d{3,8}
表示3-8个数字,例如'1234567'
。
综合起来,上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。
如果要匹配'010-12345'
这样的号码呢?由于'-'
是特殊字符,在正则表达式中,要用'\'
转义,所以,上面的正则是\d{3}\-\d{3,8}
。
但是,仍然无法匹配'010 - 12345'
,因为带有空格。所以我们需要更复杂的匹配方式。
12.2进阶
要做更精确地匹配,可以用[]
表示范围,比如:
[0-9a-zA-Z\_]
可以匹配一个数字、字母或者下划线;[0-9a-zA-Z\_]+
可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100'
,'0_Z'
,'Py3000'
等等;[a-zA-Z\_][0-9a-zA-Z\_]*
可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量;[a-zA-Z\_][0-9a-zA-Z\_]{0, 19}
更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。
A|B
可以匹配A或B,所以(P|p)ython
可以匹配'Python'
或者'python'
。
^
表示行的开头,^\d
表示必须以数字开头。
$
表示行的结束,\d$
表示必须以数字结束。
你可能注意到了,py
也可以匹配'python'
,但是加上^py$
就变成了整行匹配,就只能匹配'py'
了。
12.3re模块
Python提供re
模块,包含所有正则表达式的功能。由于Python的字符串本身也用\
转义,所以要特别注意:
1 | s = 'ABC\\-001' # Python的字符串 |
因此我们强烈建议使用Python的r
前缀,就不用考虑转义的问题了:
1 | s = r'ABC\-001' # Python的字符串 |
先看看如何判断正则表达式是否匹配:
1 | import re |
match()
方法判断是否匹配,如果匹配成功,返回一个Match
对象,否则返回None
。常见的判断方法就是:
1 | test = '用户输入的字符串' |
match
只从字串的开始位置进行匹配,如果失败,它就此放弃;
search
则会锲而不舍地完全遍历整个字串中所有可能的位置,直到成功地找到一个匹配,或者搜索完字串,以失败告终。如果你了解match的特性(在某些情况下比较快),大可以自由用它;如果不太清楚,search通常是你需要的那个函数。
从一堆文本中,找出所有可能的匹配,以列表的形式返回,这种情况用
findall
这个函数
12.4切分字符串
用正则表达式切分字符串比用固定的字符更灵活,请看正常的切分代码:
1 | 'a b c'.split(' ') |
嗯,无法识别连续的空格,用正则表达式试试:
1 | r'\s+', 'a b c') re.split( |
无论多少个空格都可以正常分割。加入,
试试:
1 | r'[\s\,]+', 'a,b, c d') re.split( |
再加入;
试试:
1 | r'[\s\,\;]+', 'a,b;; c d') re.split( |
如果用户输入了一组标签,下次记得用正则表达式来把不规范的输入转化成正确的数组。
12.5分组
除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()
表示的就是要提取的分组(Group)。比如:
^(\d{3})-(\d{3,8})$
分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:
1 | r'^(\d{3})-(\d{3,8})$', '010-12345') m = re.match( |
如果正则表达式中定义了组,就可以在Match
对象上用group()
方法提取出子串来。
注意到group(0)
永远是原始字符串,group(1)
、group(2)
……表示第1、2、……个子串。
提取子串非常有用。来看一个更凶残的例子:
1 | '19:05:30' t = |
这个正则表达式可以直接识别合法的时间。但是有些时候,用正则表达式也无法做到完全验证,比如识别日期:
1 | '^(0[1-9]|1[0-2]|[0-9])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$' |
对于'2-30'
,'4-31'
这样的非法日期,用正则还是识别不了,或者说写出来非常困难,这时就需要程序配合识别了。
12.6贪婪匹配
正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0
:
1 | r'^(\d+)(0*)$', '102300').groups() re.match( |
由于\d+
采用贪婪匹配,直接把后面的0
全部匹配了,结果0*
只能匹配空字符串了。
必须让\d+
采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0
匹配出来,加个?
就可以让\d+
采用非贪婪匹配:
1 | r'^(\d+?)(0*)$', '102300').groups() re.match( |
12.7编译
当我们在Python中使用正则表达式时,re模块内部会干两件事情:
- 编译正则表达式,如果正则表达式的字符串本身不合法,会报错;
- 用编译后的正则表达式去匹配字符串。
如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式,接下来重复使用时就不需要编译这个步骤了,直接匹配:
1 | import re |
编译后生成Regular Expression对象,由于该对象自己包含了正则表达式,所以调用对应的方法时不用给出正则字符串。
十三、常用内置模块
13.1datetime
13.1.1获取当前日期和时间
1 | from datetime import datetime |
注意到datetime
是模块,datetime
模块还包含一个datetime
类,通过from datetime import datetime
导入的才是datetime
这个类。
如果仅导入import datetime
,则必须引用全名datetime.datetime
。
datetime.now()
返回当前日期和时间,其类型是datetime
。
13.1.2获取指定日期和时间
直接用参数构造一个datetime
:
1 | from datetime import datetime |
13.1.3datetime转换为timestamp
在计算机中,时间实际上是用数字表示的。我们把1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time,记为0
(1970年以前的时间timestamp为负数),当前时间就是相对于epoch time的秒数,称为timestamp。
你可以认为:
1 | timestamp = 0 = 1970-1-1 00:00:00 UTC+0:00 |
对应的北京时间是:
1 | timestamp = 0 = 1970-1-1 08:00:00 UTC+8:00 |
可见timestamp的值与时区毫无关系,因为timestamp一旦确定,其UTC时间就确定了,转换到任意时区的时间也是完全确定的,这就是为什么计算机存储的当前时间是以timestamp表示的,因为全球各地的计算机在任意时刻的timestamp都是完全相同的(假定时间已校准)。
把一个datetime
类型转换为timestamp只需要简单调用timestamp()
方法:
1 | from datetime import datetime |
注意Python的timestamp是一个浮点数。如果有小数位,小数位表示毫秒数。
某些编程语言(如Java和JavaScript)的timestamp使用整数表示毫秒数,这种情况下只需要把timestamp除以1000就得到Python的浮点表示方法。
13.1.4timestamp转换为datetime
使用datetime
提供的fromtimestamp()
方法:
1 | from datetime import datetime |
注意到timestamp是一个浮点数,它没有时区的概念,而datetime是有时区的。上述转换是在timestamp和本地时间(当前操作系统设定的时区)做转换。
例如北京时区是东8区,则本地时间:
1 | 2015-04-19 12:20:00 |
实际上就是UTC+8:00时区的时间:
1 | 2015-04-19 12:20:00 UTC+8:00 |
而此刻的格林威治标准时间与北京时间差了8小时,也就是UTC+0:00时区的时间应该是:
1 | 2015-04-19 04:20:00 UTC+0:00 |
timestamp也可以直接被转换到UTC标准时区的时间:
1 | from datetime import datetime |
13.1.5str转换为datetime
很多时候,用户输入的日期和时间是字符串,要处理日期和时间,首先必须把str转换为datetime。通过datetime.strptime()
实现即可,需要一个日期和时间的格式化字符串:
1 | from datetime import datetime |
字符串'%Y-%m-%d %H:%M:%S'
规定了日期和时间部分的格式。详细的说明请参考Python文档。
注意转换后的datetime是没有时区信息的。
13.1.6datetime转换为str
如果已经有了datetime对象,要把它格式化为字符串显示给用户,就需要转换为str,转换方法是通过strftime()
实现的,同样需要一个日期和时间的格式化字符串:
1 | from datetime import datetime |
13.1.7datetime加减
加减可以直接用+
和-
运算符,不过需要导入timedelta
这个类:
1 | from datetime import datetime, timedelta |
13.1.8本地时间转换为UTC时间
本地时间是指系统设定时区的时间,例如北京时间是UTC+8:00时区的时间,而UTC时间指UTC+0:00时区的时间。
一个datetime
类型有一个时区属性tzinfo
,但是默认为None
,所以无法区分这个datetime
到底是哪个时区,除非强行给datetime
设置一个时区:
1 | from datetime import datetime, timedelta, timezone |
如果系统时区恰好是UTC+8:00,那么上述代码就是正确的,否则,不能强制设置为UTC+8:00时区。
13.1.9时区转换
先通过utcnow()
拿到当前的UTC时间,再转换为任意时区的时间:
1 | # 拿到UTC时间,并强制设置时区为UTC+0:00: |
时区转换的关键在于,拿到一个datetime
时,要获知其正确的时区,然后强制设置时区,作为基准时间。
利用带时区的datetime
,通过astimezone()
方法,可以转换到任意时区。
注:不是必须从UTC+0:00时区转换到其他时区,任何带时区的datetime
都可以正确转换,例如上述bj_dt
到tokyo_dt
的转换。
总结:
datetime
表示的时间需要时区信息才能确定一个特定的时间,否则只能视为本地时间。
如果要存储datetime
,最佳方法是将其转换为timestamp再存储,因为timestamp的值与时区完全无关。
13.2collections
13.2.1namedtuple
tuple
可以表示不变集合,如一个点的二维坐标:
1 | 1, 2) p = ( |
但是,很难看出这个tuple
是用来表示一个坐标的。
定义一个class又小题大做了,这时,namedtuple
就派上了用场:
1 | from collections import namedtuple |
namedtuple
是一个函数,它用来创建一个自定义的tuple
对象,并且规定了tuple
元素的个数,并可以用属性而不是索引来引用tuple
的某个元素。
这样一来,我们用namedtuple
可以很方便地定义一种数据类型,它具备tuple的不变性,又可以根据属性来引用,使用十分方便。
可以验证创建的Point
对象是tuple
的一种子类:
1 | isinstance(p, Point) |
类似的,如果要用坐标和半径表示一个圆,也可以用namedtuple
定义:
1 | # namedtuple('名称', [属性list]): |
13.2.2deque
使用list
存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list
是线性存储,数据量大的时候,插入和删除效率很低。
deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈:
1 | from collections import deque |
deque
除了实现list的append()
和pop()
外,还支持appendleft()
和popleft()
,这样就可以非常高效地往头部添加或删除元素。
13.2.3defaultdict
使用dict
时,如果引用的Key不存在,就会抛出KeyError
。如果希望key不存在时,返回一个默认值,就可以用defaultdict
:
1 | from collections import defaultdict |
注意默认值是调用函数返回的,而函数在创建defaultdict
对象时传入。
除了在Key不存在时返回默认值,defaultdict
的其他行为跟dict
是完全一样的。
13.2.4OrderedDict
使用dict
时,Key是无序的。在对dict
做迭代时,我们无法确定Key的顺序。
如果要保持Key的顺序,可以用OrderedDict
:
1 | from collections import OrderedDict |
注意,OrderedDict
的Key会按照插入的顺序排列,不是Key本身排序:
1 | od = OrderedDict() |
OrderedDict
可以实现一个FIFO(先进先出)的dict,当容量超出限制时,先删除最早添加的Key:
1 | from collections import OrderedDict |
13.2.5ChainMap
ChainMap
可以把一组dict
串起来并组成一个逻辑上的dict
。ChainMap
本身也是一个dict,但是查找的时候,会按照顺序在内部的dict依次查找。
什么时候使用ChainMap
最合适?举个例子:应用程序往往都需要传入参数,参数可以通过命令行传入,可以通过环境变量传入,还可以有默认参数。我们可以用ChainMap
实现参数的优先级查找,即先查命令行参数,如果没有传入,再查环境变量,如果没有,就使用默认参数。
下面的代码演示了如何查找user
和color
这两个参数:
1 | from collections import ChainMap |
没有任何参数时,打印出默认参数:
1 | $ python3 use_chainmap.py |
当传入命令行参数时,优先使用命令行参数:
1 | $ python3 use_chainmap.py -u bob |
同时传入命令行参数和环境变量,命令行参数的优先级较高:
1 | $ user=admin color=green python3 use_chainmap.py -u bob |
13.2.6Counter
Counter
是一个简单的计数器,例如,统计字符出现的个数:
1 | from collections import Counter |
Counter
实际上也是dict
的一个子类,上面的结果可以看出每个字符出现的次数。
13.3base64
Base64是一种任意二进制到文本字符串的编码方法,常用于在URL、Cookie、网页中传输少量二进制数据。
用记事本打开exe
、jpg
、pdf
这些文件时,我们都会看到一大堆乱码,因为二进制文件包含很多无法显示和打印的字符,所以,如果要让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法。
Base64的原理很简单,首先,准备一个包含64个字符的数组:
1 | ['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/'] |
然后,对二进制数据进行处理,每3个字节一组,一共是3x8=24
bit,划为4组,每组正好6个bit
这样我们得到4个数字作为索引,然后查表,获得相应的4个字符,就是编码后的字符串。
所以,Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。
如果要编码的二进制数据不是3的倍数,最后会剩下1个或2个字节怎么办?Base64用\x00
字节在末尾补足后,再在编码的末尾加上1个或2个=
号,表示补了多少字节,解码的时候,会自动去掉。
Python内置的base64
可以直接进行base64的编解码:
1 | import base64 |
由于标准的Base64编码后可能出现字符+
和/
,在URL中就不能直接作为参数,所以又有一种"url safe"的base64编码,其实就是把字符+
和/
分别变成-
和_
:
1 | b'i\xb7\x1d\xfb\xef\xff') base64.b64encode( |
还可以自己定义64个字符的排列顺序,这样就可以自定义Base64编码,不过通常情况下完全没有必要。
Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。
Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等。
由于=
字符也可能出现在Base64编码中,但=
用在URL、Cookie里面会造成歧义,所以,很多Base64编码后会把=
去掉。
13.4struct
准确地讲,Python没有专门处理字节的数据类型。但由于b'str'
可以表示字节,所以,字节数组=二进制str。而在C语言中,我们可以很方便地用struct、union来处理字节,以及字节和int,float的转换。
在Python中,比方说要把一个32位无符号整数变成字节,也就是4个长度的bytes
,你得配合位运算符这么写:
1 | 10240099 n = |
非常麻烦。如果换成浮点数就无能为力了。
好在Python提供了一个struct
模块来解决bytes
和其他二进制数据类型的转换。
struct
的pack
函数把任意数据类型变成bytes
:
1 | import struct |
pack
的第一个参数是处理指令,'>I'
的意思是:
>
表示字节顺序是big-endian,也就是网络序,I
表示4字节无符号整数。
后面的参数个数要和处理指令一致。
unpack
把bytes
变成相应的数据类型:
1 | '>IH', b'\xf0\xf0\xf0\xf0\x80\x80') struct.unpack( |
根据>IH
的说明,后面的bytes
依次变为I
:4字节无符号整数和H
:2字节无符号整数。
struct
模块定义的数据类型可以参考Python官方文档:
https://docs.python.org/zh-cn/3/library/struct.html#format-characters
Windows的位图文件(.bmp)是一种非常简单的文件格式,我们来用
struct
分析一下。首先找一个bmp文件,没有的话用“画图”画一个。
读入前30个字节来分析:
1 b'\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00' s =BMP格式采用小端方式存储数据,文件头的结构按顺序如下:
两个字节:
'BM'
表示Windows位图,'BA'
表示OS/2位图; 一个4字节整数:表示位图大小; 一个4字节整数:保留位,始终为0; 一个4字节整数:实际图像的偏移量; 一个4字节整数:Header的字节数; 一个4字节整数:图像宽度; 一个4字节整数:图像高度; 一个2字节整数:始终为1; 一个2字节整数:颜色数。所以,组合起来用
unpack
读取:
1
2 '<ccIIIIIIHH', s) struct.unpack(
(b'B', b'M', 691256, 0, 54, 40, 640, 360, 1, 24)结果显示,
b'B'
、b'M'
说明是Windows位图,位图大小为640x360,颜色数为24。
13.5hashlib
Python的hashlib提供了常见的摘要算法(又称哈希算法、散列算法),如MD5,SHA1等等。
我们以常见的摘要算法MD5为例,计算出一个字符串的MD5值:
1 | import hashlib |
计算结果如下:
1 | d26a53750bc40b38b65a520292f69306 |
如果数据量很大,可以分块多次调用update()
,最后计算的结果是一样的:
1 | import hashlib |
MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,通常用一个32位的16进制字符串表示。
另一种常见的摘要算法是SHA1,调用SHA1和调用MD5完全类似:
1 | import hashlib |
SHA1的结果是160 bit字节,通常用一个40位的16进制字符串表示。
比SHA1更安全的算法是SHA256和SHA512,不过越安全的算法不仅越慢,而且摘要长度更长。
有没有可能两个不同的数据通过某个摘要算法得到了相同的摘要?完全有可能,因为任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。这种情况称为碰撞,比如Bob试图根据你的摘要反推出一篇文章'how to learn hashlib in python - by Bob'
,并且这篇文章的摘要恰好和你的文章完全一致,这种情况也并非不可能出现,但是非常非常困难。
采用MD5存储口令是否就一定安全呢?也不一定。假设你是一个黑客,已经拿到了存储MD5口令的数据库,如何通过MD5反推用户的明文口令呢?暴力破解费事费力,真正的黑客不会这么干。
考虑这么个情况,很多用户喜欢用123456
,888888
,password
这些简单的口令,于是,黑客可以事先计算出这些常用口令的MD5值,得到一个反推表:
1 | 'e10adc3949ba59abbe56e057f20f883e': '123456' |
这样,无需破解,只需要对比数据库的MD5,黑客就获得了使用常用口令的用户账号。
对于用户来讲,当然不要使用过于简单的口令。但是,我们能否在程序设计上对简单口令加强保护呢?
由于常用口令的MD5值很容易被计算出来,所以,要确保存储的用户口令不是那些已经被计算出来的常用口令的MD5,这一方法通过对原始口令加一个复杂字符串来实现,俗称“加盐”:
1 | def calc_md5(password): |
经过Salt处理的MD5口令,只要Salt不被黑客知道,即使用户输入简单口令,也很难通过MD5反推明文口令。
但是如果有两个用户都使用了相同的简单口令比如123456
,在数据库中,将存储两条相同的MD5值,这说明这两个用户的口令是一样的。有没有办法让使用相同口令的用户存储不同的MD5呢?
如果假定用户无法修改登录名,就可以通过把登录名作为Salt的一部分来计算MD5,从而实现相同口令的用户也存储不同的MD5。
13.6hmac
通过哈希算法,我们可以验证一段数据是否有效,方法就是对比该数据的哈希值,例如,判断用户口令是否正确,我们用保存在数据库中的password_md5
对比计算md5(password)
的结果,如果一致,用户输入的口令就是正确的。
为了防止黑客通过彩虹表根据哈希值反推原始口令,在计算哈希的时候,不能仅针对原始输入计算,需要增加一个salt来使得相同的输入也能得到不同的哈希,这样,大大增加了黑客破解的难度。
如果salt是我们自己随机生成的,通常我们计算MD5时采用md5(message + salt)
。但实际上,把salt看做一个“口令”,加salt的哈希就是:计算一段message的哈希时,根据不通口令计算出不同的哈希。要验证哈希值,必须同时提供正确的口令。
这实际上就是Hmac算法:Keyed-Hashing for Message Authentication。它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。
和我们自定义的加salt算法不同,Hmac算法针对所有哈希算法都通用,无论是MD5还是SHA-1。采用Hmac替代我们自己的salt算法,可以使程序算法更标准化,也更安全。
Python自带的hmac模块实现了标准的Hmac算法,它利用一个key对message计算“杂凑”后的hash,使用hmac算法比标准hash算法更安全,因为针对相同的message,不同的key会产生不同的hash。
我们首先需要准备待计算的原始消息message,随机key,哈希算法,这里采用MD5,使用hmac的代码如下:
1 | import hmac |
可见使用hmac和普通hash算法非常类似。hmac输出的长度和原始哈希算法的长度一致。需要注意传入的key和message都是bytes
类型,str
类型需要首先编码为bytes
。
13.7itertools
Python的内建模块itertools
提供了非常有用的用于操作迭代对象的函数。
首先,我们看看itertools
提供的几个“无限”迭代器:
1 | import itertools |
因为count()
会创建一个无限的迭代器,所以上述代码会打印出自然数序列,根本停不下来,只能按Ctrl+C
退出。
cycle()
会把传入的一个序列无限重复下去:
1 | import itertools |
同样停不下来。
repeat()
负责把一个元素无限重复下去,不过如果提供第二个参数就可以限定重复次数:
1 | 'A', 3) ns = itertools.repeat( |
无限序列只有在for
迭代时才会无限地迭代下去,如果只是创建了一个迭代对象,它不会事先把无限个元素生成出来,事实上也不可能在内存中创建无限多个元素。
无限序列虽然可以无限迭代下去,但是通常我们会通过takewhile()
等函数根据条件判断来截取出一个有限的序列:
1 | 1) natuals = itertools.count( |
itertools
提供的几个迭代器操作函数更加有用:
13.7.1chain()
chain()
可以把一组迭代对象串联起来,形成一个更大的迭代器:
1 | for c in itertools.chain('ABC', 'XYZ'): |
13.7.2groupby()
groupby()
把迭代器中相邻的重复元素挑出来放在一起:
1 | for key, group in itertools.groupby('AAABBBCCAAA'): |
实际上挑选规则是通过函数完成的,只要作用于函数的两个元素返回的值相等,这两个元素就被认为是在一组的,而函数返回值作为组的key。如果我们要忽略大小写分组,就可以让元素'A'
和'a'
都返回相同的key:
1 | for key, group in itertools.groupby('AaaBBbcCAAa', lambda c: c.upper()): |
13.8contextlib
在Python中,读写文件这样的资源要特别注意,必须在使用完毕后正确关闭它们。正确关闭文件资源的一个方法是使用try...finally
:
1 | try: |
写try...finally
非常繁琐。Python的with
语句允许我们非常方便地使用资源,而不必担心资源没有关闭,所以上面的代码可以简化为:
1 | with open('/path/to/file', 'r') as f: |
并不是只有open()
函数返回的fp对象才能使用with
语句。实际上,任何对象,只要正确实现了上下文管理,就可以用于with
语句。
实现上下文管理是通过__enter__
和__exit__
这两个方法实现的。例如,下面的class实现了这两个方法:
1 | class Query(object): |
这样我们就可以把自己写的资源对象用于with
语句:
1 | with Query('Bob') as q: |
13.8.1@contextmanager
编写__enter__
和__exit__
仍然很繁琐,因此Python的标准库contextlib
提供了更简单的写法,上面的代码可以改写如下:
1 | from contextlib import contextmanager |
@contextmanager
这个decorator接受一个generator,用yield
语句把with ... as var
把变量输出出去,然后,with
语句就可以正常地工作了:
1 | with create_query('Bob') as q: |
很多时候,我们希望在某段代码执行前后自动执行特定代码,也可以用@contextmanager
实现。例如:
1 |
|
上述代码执行结果为:
1 | <h1> |
代码的执行顺序是:
with
语句首先执行yield
之前的语句,因此打印出<h1>
;yield
调用会执行with
语句内部的所有语句,因此打印出hello
和world
;- 最后执行
yield
之后的语句,打印出</h1>
。
因此,@contextmanager
让我们通过编写generator来简化上下文管理。
13.8.2@closing
如果一个对象没有实现上下文,我们就不能把它用于with
语句。这个时候,可以用closing()
来把该对象变为上下文对象。例如,用with
语句使用urlopen()
:
1 | from contextlib import closing |
closing
也是一个经过@contextmanager装饰的generator,这个generator编写起来其实非常简单:
1 |
|
它的作用就是把任意对象变为上下文对象,并支持with
语句。
@contextlib
还有一些其他decorator,便于我们编写更简洁的代码。
13.9urllib
urllib提供了一系列用于操作URL的功能。
13.9.1Get
urllib的request
模块可以非常方便地抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的响应:
例如,对豆瓣的一个URLhttps://api.douban.com/v2/book/2129650
进行抓取,并返回响应:
1 | from urllib import request |
可以看到HTTP响应的头和JSON数据:
1 | Status: 200 OK |
如果我们要想模拟浏览器发送GET请求,就需要使用Request
对象,通过往Request
对象添加HTTP头,我们就可以把请求伪装成浏览器。例如,模拟iPhone 6去请求豆瓣首页:
1 | from urllib import request |
这样豆瓣会返回适合iPhone的移动版网页:
1 | ... |
13.9.2Post
如果要以POST发送一个请求,只需要把参数data
以bytes形式传入。
我们模拟一个微博登录,先读取登录的邮箱和口令,然后按照weibo.cn的登录页的格式以username=xxx&password=xxx
的编码传入:
1 | from urllib import request, parse |
如果登录成功,我们获得的响应如下:
1 | Status: 200 OK |
如果登录失败,我们获得的响应如下:
1 | ... |
13.9.3Handler
如果还需要更复杂的控制,比如通过一个Proxy去访问网站,我们需要利用ProxyHandler
来处理,示例代码如下:
1 | proxy_handler = urllib.request.ProxyHandler({'http': 'http://www.example.com:3128/'}) |
urllib提供的功能就是利用程序去执行各种HTTP请求。如果要模拟浏览器完成特定功能,需要把请求伪装成浏览器。伪装的方法是先监控浏览器发出的请求,再根据浏览器的请求头来伪装,User-Agent
头就是用来标识浏览器的。
13.10venv/virtualenv
这一块可以参考官方文档:12. 虚拟环境和包 — Python 3.9.4 文档
这两个的区别:
- virtualenv:Python 虚拟环境管理工具。
- venv:Python 标准库内置的虚拟环境管理工具,Python 3.3 加入,Python 3.5 开始作为管理虚拟环境的推荐工具,用法类似 virtualenv。(Linux上需要apt安装)
- venv不能指定python的版本,用什么版本的python创建的虚拟环境,虚拟环境中的python就是什么版本
如果使用 Python 3,推荐使用
venv
来替代virtualenv
。
13.10.1创建虚拟环境
一般推荐将虚拟环境创建在对应的项目下。具体创建命令:
1 | # venv |
- 会在当前目录下创建名为
虚拟环境名称
的文件夹,其中是该环境的一些包与依赖 - Ubuntu上venv需要通过apt安装:
sudo apt install python3-venv
- 注意python3的版本,Ubuntu上如果有多个python3,可能需要python3.x来指定对应的python版本
13.10.2激活虚拟环境
通过执行对应的激活脚本(在上一步创建的虚拟环境文件夹中)来激活虚拟环境,不同操作系统的激活命令(激活脚本及路径)不太相同:
-
Windows:
1
虚拟环境名称\scripts\activate
-
Linux/macOS:
1
2
3source 虚拟环境名称/bin/activate
# 或
. 虚拟环境名称/bin/activate
13.10.3导入全局包到虚拟环境中
对于ubuntu来说,有些包在虚拟环境中安装比较麻烦或者不能通过虚拟环境安装,而直接通过apt安装比pip安装快很多,此时需要将全局包导入到虚拟环境中。这里以pyqt5为例
首先需要安装pyqt
1 | sudo apt install python3-pyqt5 |
直接将系统环境中的包复制到虚拟环境中即可:
1 | cp -R /usr/lib/python3/dist-packages/PyQt5 ./venv/lib/python3.8/site-packages/ |
- 一般来说,系统中的python包路径在
/usr/lib/python3/dist-packages
下,而虚拟环境中的python包路径在./venv/lib/pythonXXX/site-packages/
下
13.10.4退出虚拟环境
执行退出命令即可:
1 | deactivate |
13.10.5删除虚拟环境
在保证没有激活该虚拟环境的情况下,直接删除创建的文件夹即可
13.10.6自动激活虚拟环境
以下几个插件均能够自动激活虚拟环境:
- direnv: unclutter your .profile
- zsh-autoenv: Autoenv for zsh(仅限zsh)
- autoenv: Directory-based environments
这里使用的是autoenv:
-
安装:
1
2
3
4# 安装
pip install autoenv
# 修改配置文件
$ echo "source `which activate.sh`" >> ~/.bashrc- 如果找不到
activate.sh
,一般都在~/.local/bin/activate.sh
- 如果找不到
-
使用:通过下面的命令在项目所在文件夹 project 内新建
.env
文件,并在该文件内写入进入项目环境的命令1
echo "source venv/bin/activate" > project/.env
- 离开的文件名默认为
.env.leave
,可以不添加该文件 - 注意,添加
.env.leave
时需要开启宏AUTOENV_ENABLE_LEAVE
(非空即可,如:export AUTOENV_ENABLE_LEAVE='enable'
)才可用
- 离开的文件名默认为
十四、常用第三方模块
基本上,所有的第三方模块都会在PyPI - the Python Package Index上注册,只要找到对应的模块名字,即可用pip安装。
14.1Pillow
PIL:Python Imaging Library,已经是Python平台事实上的图像处理标准库了。PIL功能非常强大,但API却非常简单易用。
由于PIL仅支持到Python 2.7,加上年久失修,于是一群志愿者在PIL的基础上创建了兼容的版本,名字叫Pillow,支持最新Python 3.x,又加入了许多新特性,因此,我们可以直接安装使用Pillow。
如果安装了Anaconda,Pillow就已经可用了。否则,需要在命令行下通过pip安装:
1 | $ pip install pillow |
14.1.1操作图像
来看看最常见的图像缩放操作,只需三四行代码:
1 | from PIL import Image |
其他功能如切片、旋转、滤镜、输出文字、调色板等一应俱全。
比如,模糊效果也只需几行代码:
1 | from PIL import Image, ImageFilter |
效果如下:
PIL的ImageDraw
提供了一系列绘图方法,让我们可以直接绘图。比如要生成字母验证码图片:
1 | from PIL import Image, ImageDraw, ImageFont, ImageFilter |
我们用随机颜色填充背景,再画上文字,最后对图像进行模糊,得到验证码图片如下:
如果运行的时候报错:
1 | IOError: cannot open resource |
这是因为PIL无法定位到字体文件的位置,可以根据操作系统提供绝对路径,比如:
1 | '/Library/Fonts/Arial.ttf' |
要详细了解PIL的强大功能,请请参考Pillow官方文档:
https://pillow.readthedocs.org/
14.2requests
Python内置的urllib模块用于访问网络资源,但是用起来比较麻烦,而且缺少很多实用的高级功能。
更好的方案是使用requests。它是一个Python第三方库,处理URL资源特别方便。
如果安装了Anaconda,requests就已经可用了。否则,需要在命令行下通过pip安装:pip install requests
要通过GET访问一个页面,只需要几行代码:
1 | import requests |
对于带参数的URL,传入一个dict作为params
参数:
1 | 'https://www.douban.com/search', params={'q': 'python', 'cat': '1001'}) r = requests.get( |
requests自动检测编码,可以使用encoding
属性查看:
1 | r.encoding |
无论响应是文本还是二进制内容,我们都可以用content
属性获得bytes
对象:
1 | r.content |
requests的方便之处还在于,对于特定类型的响应,例如JSON,可以直接获取:
1 | 'https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20%3D%202151330&format=json') r = requests.get( |
需要传入HTTP Header时,我们传入一个dict作为headers
参数:
1 | 'https://www.douban.com/', headers={'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit'}) r = requests.get( |
要发送POST请求,只需要把get()
方法变成post()
,然后传入data
参数作为POST请求的数据:
1 | 'https://accounts.douban.com/login', data={'form_email': 'abc@example.com', 'form_password': '123456'}) r = requests.post( |
requests默认使用application/x-www-form-urlencoded
对POST数据编码。如果要传递JSON数据,可以直接传入json参数:
1 | params = {'key': 'value'} |
类似的,上传文件需要更复杂的编码格式,但是requests把它简化成files
参数:
1 | 'file': open('report.xls', 'rb')} upload_files = { |
在读取文件时,注意务必使用'rb'
即二进制模式读取,这样获取的bytes
长度才是文件的长度。
把post()
方法替换为put()
,delete()
等,就可以以PUT或DELETE方式请求资源。
除了能轻松获取响应内容外,requests对获取HTTP响应的其他信息也非常简单。例如,获取响应头:
1 | r.headers |
requests对Cookie做了特殊处理,使得我们不必解析Cookie就可以轻松获取指定的Cookie:
1 | 'ts'] r.cookies[ |
要在请求中传入Cookie,只需准备一个dict传入cookies
参数:
1 | 'token': '12345', 'status': 'working'} cs = { |
最后,要指定超时,传入以秒为单位的timeout参数:
1 | 2.5) # 2.5秒后超时 r = requests.get(url, timeout= |
14.3chardet
字符串编码一直是令人非常头疼的问题,尤其是我们在处理一些不规范的第三方网页的时候。虽然Python提供了Unicode表示的str
和bytes
两种数据类型,并且可以通过encode()
和decode()
方法转换,但是,在不知道编码的情况下,对bytes
做decode()
不好做。
对于未知编码的bytes
,要把它转换成str
,需要先“猜测”编码。猜测的方式是先收集各种编码的特征字符,根据特征字符判断,就能有很大概率“猜对”。
当然,我们肯定不能从头自己写这个检测编码的功能,这样做费时费力。chardet这个第三方库正好就派上了用场。用它来检测编码,简单易用。
如果安装了Anaconda,chardet就已经可用了。否则,需要在命令行下通过pip安装:pip install chardet
当我们拿到一个bytes
时,就可以对其检测编码。用chardet检测编码,只需要一行代码:
1 | b'Hello, world!') chardet.detect( |
检测出的编码是ascii
,注意到还有个confidence
字段,表示检测的概率是1.0(即100%)。
再试试检测GBK编码的中文:
1 | '离离原上草,一岁一枯荣'.encode('gbk') data = |
检测的编码是GB2312
,注意到GBK是GB2312的超集,两者是同一种编码,检测正确的概率是74%,language
字段指出的语言是'Chinese'
。
对UTF-8编码进行检测:
1 | '离离原上草,一岁一枯荣'.encode('utf-8') data = |
我们再试试对日文进行检测:
1 | '最新の主要ニュース'.encode('euc-jp') data = |
可见,用chardet检测编码,使用简单。获取到编码后,再转换为str
,就可以方便后续处理。
chardet支持检测的编码列表请参考官方文档Supported encodings。
14.4psutil
要获取一些系统信息,Python可以通过subprocess
模块调用并获取结果。但这样做显得很麻烦,尤其是要写很多解析代码。
在Python中获取系统信息的另一个好办法是使用psutil
( process and system utilities)这个第三方模块。它不仅可以通过一两行代码实现系统监控,还支持Linux/UNIX/OSX/Windows等。更多信息:https://github.com/giampaolo/psutil
如果安装了Anaconda,psutil就已经可用了。否则,需要在命令行下通过pip安装:pip install psutil
14.4.1获取CPU信息
1 | import psutil |
统计CPU的用户/系统/空闲时间:
1 | psutil.cpu_times() |
再实现类似top
命令的CPU使用率,每秒刷新一次,累计10次:
1 | for x in range(10): |
14.4.2获取内存信息
使用psutil获取物理内存和交换内存信息,分别使用:
1 | psutil.virtual_memory() |
返回的是字节为单位的整数,可以看到,总内存大小是8589934592 = 8 GB,已用7201386496 = 6.7 GB,使用了66.6%。
而交换区大小是1073741824 = 1 GB。
14.4.3获取磁盘信息
可以通过psutil获取磁盘分区、磁盘使用率和磁盘IO信息:
1 | # 磁盘分区信息 psutil.disk_partitions() |
可以看到,磁盘'/'
的总容量是998982549504 = 930 GB,使用了39.1%。文件格式是HFS,opts
中包含rw
表示可读写,journaled
表示支持日志。
14.4.4获取网络信息
psutil可以获取网络接口和网络连接信息:
1 | # 获取网络读写字节/包的个数 psutil.net_io_counters() |
要获取当前网络连接信息,使用net_connections()
:
1 | psutil.net_connections() |
你可能会得到一个AccessDenied
错误,原因是psutil获取信息也是要走系统接口,而获取网络连接信息需要root权限,这种情况下,可以退出Python交互环境,用sudo
重新启动:
1 | $ sudo python3 |
14.4.5获取进程信息
通过psutil可以获取到所有进程的详细信息:
1 | # 所有进程ID psutil.pids() |
和获取网络连接类似,获取一个root用户的进程需要root权限,启动Python交互环境或者.py
文件时,需要sudo
权限。
psutil还提供了一个test()
函数,可以模拟出ps
命令的效果:
1 | $ sudo python3 |