Python教程

From https://www.liaoxuefeng.com/

Python简介

Python是著名的“龟叔”Guido van Rossum在1989年圣诞节期间,为了打发无聊的圣诞节而编写的一个编程语言。
龟叔给Python的定位是“优雅”、“明确”、“简单”。

那Python适合开发哪些类型的应用呢?
首选是网络应用,包括网站、后台服务等等;
其次是许多日常需要的小工具,包括系统管理员需要的脚本任务等等;
另外就是把其他语言开发的程序再包装起来,方便使用。

Python的缺点:
1.慢;
2.代码不能加密。

安装Python

因为Python是跨平台的,它可以运行在Windows、Mac和各种Linux/Unix系统上。

https://wiki.python.org/moin/BeginnersGuide/Download

Python解释器

Python源文件是一个包含Python代码的以.py为扩展名的文本文件。要运行代码,就需要Python解释器去执行.py文件。

解释器 Desc
CPython 官方版本的解释器,以C语言开发。
IPython 基于CPython的交互式解释器,执行Python代码的功能与CPython相同。CPython使用>>>作为提示符,而IPython使用In [序号]:作为提示符。
PyPy PyPy对Python代码进行动态编译,所以可以显著提高Python代码的执行速度,PyPy于CPython不完全相同,相同的Python代码在两种解释器上可能产生不同的结果 PyPy与CPython的不同点
Jython 运行在Java平台上的Python解释器
IronPython 运行在.Net平台上的Python解释器

使用最广泛的还是CPython,如果要和Java或者.Net平台交互,最好的办法还是通过网络调用来交互,确保各程序间的独立性,而不是使用Jython或者IronPython。

第一个Python程序

Python交互式环境

1
C:\Users\duxin>python
1
2
3
>>> print("hello world")
hello world
>>> exit()

OR

1
2
3
4
5
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# hello_world.py
print("hello world")
1
2
D:\test\py>python hello_world.py
hello world

文本编辑器

Sublime或者Notepad++

UTF-8 without BOM

4空格TAB 且 使用空格替换TAB

输入和输出

1
2
3
4
5
6
7
8
# 逗号处会被替换成空格
>>> print('The quick brown fox', 'jumps over', 'the lazy dog')
The quick brown fox jumps over the lazy dog
# 获取用户输入
>>> name = input()
Michael
>>> name
'Michael'
1
2
3
# hello.py
name = input('please enter your name: ')
print('hello', name)
1
2
3
D:/test/py>python hello.py
please enter your name: Michael
hello Michael

Python基础

#开头的语句是注释。

当语句以冒号:结尾时,缩进的语句视为代码块。

大小写敏感。

数据类型和变量

整数:1,100,-8080,0xff00
Python对整数没有大小限制

浮点数:1.23,3.14,-9.01,1.23e9,12.3e8
浮点数运算可能会有四舍五入的误差。
Python对浮点数也没有大小限制,但超出一定范围就直接表示为inf(infinity)。

字符串:’abc’,’xyz’,”I’m OK”,’I'm OK’
转义符:’\n’,’\t’,’\‘
r''表示''内部的字符串默认不进行转义。

1
2
3
4
desc = '''this is line1
this is line2
this is line3'''
print(desc)

''' '''中可以包含多行内容,r''' '''多行且不转义。

布尔值:TrueFalse
布尔运算:andornot

空值:None

变量:变量名必须是大小写英文,数字和_组成,且不能用数字开头。
同一个变量可以反复赋值,且可以是不同类型的变量。

常量:在Python中,通常用全部大写的变量名表示常量PI = 3.14159265359
但事实上PI仍然是一个变量,Python没有任何机制保证PI不会被改变

浮点除:10 / 3结果为3.33333333333333359 / 3结果为3.0
地板除:10 // 3结果为3
取余:10 % 3结果为1

Python支持多种数据类型,在计算机内部,可以把任何数据都看出一个“对象”,而变量就是在程序中用来指向这些数据对象的,对变量赋值就是把数据和变量给关联起来。

字符串和编码

字符 ASCII Unicode UTF-8
A 01000001 00000000 01000001 01000001
X 01001110 00101101 11100100 10111000 10101101

在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。

编辑文件时,从文件中读取的UTF-8字符被转换为Unicode字符到内存中,编辑完成后,保存时再把Unicode转换为UTF-8保存到文件。

浏览网页时,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器。

Python字符串是以Unicode编码的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 获取字符的整数表示
>>> ord('A')
65
>>> ord('中')
20013

# 把编码转换为对应的字符
>>> chr(66)
'B'
>>> chr(25991)
'文'

# 十六进制
>>> '\u4e2d\u6587'
'中文'

由于Python的字符串类型str,在内存中以Unicode表示,一个字符对应若干个字节。
如果要在网络上传输,或者保存到磁盘上,就需要把str变为以字节为单位的bytes
Python对bytes类型的数据用带b前缀的单引号或双引号表示:x = b'ABC'

以Unicode表示的str通过encode()方法可以编码为指定的bytes

1
2
3
4
5
6
7
>>> 'ABC'.encode('ascii')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'

# Error ascii不能表示中文
>>> '中文'.encode('ascii')

反过来,如果需要从网络或磁盘上读取了字节流,那么读到的数据就是bytes,把bytes变为str,需要使用decode()方法:

1
2
3
4
>>> b'ABC'.decode('ascii')
'ABC'
>>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'

Python中的len()函数计算的是str的字符数,如果换成byteslen()计算的就是字节数。

1
2
3
4
5
6
7
8
9
10
>>> len('ABC')
3
>>> len('中文')
2
>>> len(b'ABC')
3
>>> len(b'\xe4\xb8\xad\xe6\x96\x87')
6
>>> len('中文'.encode('utf-8'))
6

由于Python的源代码也是一个文本文件,当源代码中包含中文,在保存源代码时,就需要务必指明保存为UTF-8编码。

1
2
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。

.py源文件必须使用UTF-8 without BOM编码保存。

在Python中,采用的格式化方式和C语言是一致的,用%实现:

1
2
3
4
>>> 'Hello %s' % 'World'
'Hello World'
>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)
'Hi, Michael, you have $1000000.'
占位符 Desc
%d 整数
%f 浮点数
%s 字符串(永远起作用)
%x 十六进制整数
%% %%来表示一个%

使用list和tuple

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# list是一个可变的有序表
>>> classmates = ['Michael', 'Bob', 'Tracy']
>>> classmates
['Michael', 'Bob', 'Tracy']
>>> len(classmates)
3
>>> classmates[0]
'Michael'
>>> classmates[1]
'Bob'
>>> classmates[2]
'Tracy'
>>> classmates[3] # Error

>>> classmates[-1]
'Tracy'
>>> classmates[-2]
'Bob'
>>> classmates[-3]
'Michael'
>>> classmates[-4] # Error

>>> classmates.append('Adam') # 追加
>>> classmates
['Michael', 'Bob', 'Tracy', 'Adam']

>>> classmates.insert(1, 'Jack') # 插入指定位置
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy', 'Adam']

>>> classmates.pop() # 删除list末尾元素
'Adam'
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy']

>>> classmates.pop(1) # 删除指定位置的元素
'Jack'
>>> classmates
['Michael', 'Bob', 'Tracy']

>>> classmates[1] = 'Sarah' # 替换元素
>>> classmates
['Michael', 'Sarah', 'Tracy']

# list中的元素类型可以不相同
>>> s = ['python', 'scheme', [123, True], 789]
>>> len(s)
4
>>> s[2][1]
123

# 空list
>>> L = []
>>> len(L)
0
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
# tuple初始化后不能修改,不可变-有序
# 当tuple被定义的时候,元素就必须被确定下来
>>> classmates = ('Michael', 'Bob', 'Tracy')
>>> t = (1, 2)
>>> t
(1, 2)
>>> t = ()
>>> t
()

# 1个元素的tuple
# 使用逗号`,`来消除`()`的歧义
>>> t = (1)
>>> t # 是个整数
1
>>> t = (1,)
>>> t # 是个tuple
(1,)

# “可变的”tuple
# t[2]指向的内存地址并没有改变
>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])

list和tuple是Python内置的有序集合,一个可变,一个不可变。

条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
salary = 123
if salary >= 1000:
print("too big")
elif salary >= 500:
print("big")
else:
print("ok")

# input()返回的数据类型是str
s = input('birth: ')
# Error: s是一个str类型
if s < 2000:
print('00前')
else:
print('00后')

循环

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
37
38
39
# 遍历
names = ['Michael', 'Bob', 'Tracy']
for name in names:
print(name)

# range(101)生成0-100的整数序列
sum = 0
for x in range(101):
sum = sum + x
print(sum)

# while
sum = 0
n = 99
while n > 0:
sum = sum + n
n = n - 2
print(sum)

# break
# 结束循环
n = 1
while n <= 100:
if n > 10:
break
print(n)
n = n + 1
print('END')

# continue
# 直接开始下一次循环
n = 0
while n < 10:
n = n + 1
if n % 2 == 0:
continue
print(n)

# break continue 通常都需要配合if使用

使用dict和set

dict

Python内置字典:
使用键-值(Key-Value)存储;
dict的Key必须是不可变对象(例如不可使用list用作Key);
dict内部存放数据的顺序与Key插入的顺序是无关的;
dict以空间换取时间。

当使用list时,随着list越长,查找和插入耗时越长;而dict查找和插入速度极快,且不会随着key的增加而变慢,但dict需要消耗更多的内存。

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
>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Michael']
95

>>> d['Adam'] = 67
>>> d['Adam']
67
>>> d
{'Bob': 75, 'Michael': 95, 'Tracy': 85, 'Adam': 67}

>>> d['Jack'] = 90
>>> d['Jack']
90
>>> d['Jack'] = 88
>>> d['Jack']
88

>>> d['NotKey'] # Error Key不存在

>>> 'Thomas' in d # 判断Key是否存在
False

>>> d.get('Thomas') # Key不存在 返回None
>>> d.get('Thomas', -1) # Key不存在时 返回缺省值
-1

>>> d.pop('Bob') # 删除Key
75
>>> d
{'Michael': 95, 'Tracy': 85, 'Adam': 67}
set

set是一组Key的集合,且Key不可重复。

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
>>> s = set([1, 2, 3]) # 创建set
>>> s
{1, 2, 3}

>>> s = set([1, 1, 2, 2, 3, 3]) # set会过滤重复元素
>>> s
{1, 2, 3}

>>> s.add(4) # 添加元素到set
>>> s
{1, 2, 3, 4}
>>> s.add(4) # 重复添加 被过滤
>>> s
{1, 2, 3, 4}

>>> s.remove(4) # 删除元素
>>> s
{1, 2, 3}

>>> s1 = set([1, 2, 3])
>>> s2 = set([2, 3, 4])
>>> s1 & s2 # 交集
{2, 3}
>>> s1 | s2 # 并集
{1, 2, 3, 4}
再议不可变对象

str是不变对象,而list是可变对象。

1
2
3
4
5
6
7
8
9
10
11
12
>>> a = ['c', 'b', 'a']
>>> a.sort() # 对可变对象list进行操作
>>> a # list内容变化了
['a', 'b', 'c']


>>> a = 'abc'
>>> b = a.replace('a', 'A') # 对不可变对象str进行操作
>>> b
'Abc'
>>> a # str没有改变
'abc'

函数

在交互式命令行中可以通过调用help(FunctionName)来查看函数帮助信息,例如help(abs)

调用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> abs(100)
100
>>> abs(-20)
20
>>> abs(12.34)
12.34

>>> abs(1, 2) # TypeError 参数数量不对
>>> abs('a') # TypeError 参数类型不对

>>> max(1, 2) # 返回最大值
>>> max(1, 2, 3, -1, 5)

>>> hex(12) # 将整数转换为十六进制字符串
'0xc'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 类型转换
>>> int('123')
123
>>> int(12.34)
12
>>> float('12.34')
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False
1
2
3
4
# alias
>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1

定义函数

1
2
3
4
5
def my_abs(x):
if x >= 0:
return x
else:
return -x

Python使用def语句定义函数,并依次写出函数名、括号、括号中的参数和冒号,然后在缩进块中编写函数体。
函数的返回值使用return语句返回。
如果没有return语句,函数执行完毕后也会返回结果,只是结果为None
return None可以简写为return

当把my_abs()函数定义保存为abstest.py文件后,可以文件所在目录启动Python交互式命令行,并使用from abstest import my_abs来导入my_abs()函数。(注意:abstest是文件名,不包含.py扩展名)

如果想定义一个什么事也不做的空函数,可以使用pass语句。

1
2
3
4
5
def nop():
pass

if age >= 18:
pass

实际上pass用来作为占位符,比如现在还没想好怎么写函数的代码,可以先放一个pass,让代码可以运行起来,缺少了pass,代码运行就会有语法错误。

调用函数时,如果参数的数量或者类型不正确,Python解释器会抛出TypeError,可以使用isinstance()函数进行类型检查。

1
2
3
4
5
6
7
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x

多值返回

1
2
3
4
5
6
import math

def move(x, y, step, angle = 0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny
1
2
3
4
5
6
7
8
# 多值返回实际上是返回一个tuple
>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0

>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)

函数的参数

定义函数时,参数的名字和位置确定下来,函数的接口定义就完成了。对于函数调用者来说,只需知道如何传递正确的参数,以及函数将返回什么样的值就够了。

位置参数
1
2
3
4
5
def power(x):
return x * x

power(5) # 25
power(15) # 225
1
2
3
4
5
6
7
8
9
10
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s

power(5, 2) # 25
power(5, 3) # 125
power(5) # Error
默认参数
1
2
3
4
5
6
7
8
9
def power(x, n = 2):
s = 1
while n > 0:
n = n - 1
s = s * x
return s

power(5) # 25
power(5, 2) # 25

必选参数在前,默认参数在后,否则Python解释器会报错。
当函数有多个参数时,把变化频繁的参数放在前面,变化不频繁的放在后面,变化不频繁的参数就可以作为默认参数。

1
2
3
4
5
6
7
8
9
10
>>> def add_end(L = []):
... L.append('END')
... return L
...
>>> add_end()
['END']
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

默认参数必须指向不变对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 使用不变对象来实现
>>> def add_end(L = None):
... if L is None:
... L = []
... L.append('END')
... return L
...
>>> add_end()
['END']
>>> add_end()
['END']
>>> add_end()
['END']

不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁。

在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

可变参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# calc 可接收任意个数的参数
# 并将参数组装为一个tuple
>>> def calc(*numbers):
... sum = 0
... for n in numbers:
... sum = sum + n * n
... return sum
...
>>> calc(1)
1
>>> calc(1, 3, 5)
35
>>> calc()
0
# 可在list或者tuple前加一个*号
# 将list或tuple的元素变成可变参数传进去
>>> nums = [1, 3, 5]
>>> calc(*nums)
35
关键字参数

可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。
而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。

1
2
3
4
5
6
7
8
9
10
11
12
>>> def person(name, age, **kw):
... print('name:', name, 'age:', age, 'other:', kw)
...
>>> person('Michael', 30)
name: Michael age: 30 other: []
>>> person('Bob', 35, city = 'Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender = 'M', job = 'Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict。
注意:kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra

命名关键字参数

对于关键字函数,函数调用者可以传入任意不受限制的关键字参数。
命名关键字参数可以限制关键字参数的名字

1
2
def person(name, age, *, city, job):
print(name, age, city, job)

和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。

1
2
>>> person('Jack', 24, city = 'Beijing', job = 'Engineer')
Jack 24 Beijing Engineer

如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了。

1
2
def person(name, age, *args, city, job):
print(name, age, args, city, job)

命名关键字参数必须传入参数名。
命名关键字参数可以有缺省值。

1
2
3
4
5
>>> def person(name, age, *, city = 'Beijing', job):
... print(name, age, city, jon)
...
>>> person('Jack', 24, job = 'Engineer')
Jack 24 Beijing Engineer
参数组合

在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。
但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

比如定义一个函数,包含上述若干种参数:

1
2
3
4
5
def f1(a, b, c = 0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c = 0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> f1(1, 2)
a = 1 b = 2 c = 0 args = () kw = {}
>>> f1(1, 2, c = 3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> f1(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
>>> f1(1, 2, 3, 'a', 'b', x = 99)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
>>> f2(1, 2, d = 99, ext = None)
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}
>>>
>>>
>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}

对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。

小结

Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。

默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!

要注意定义可变参数和关键字参数的语法:

*args是可变参数,args接收的是一个tuple;

**kw是关键字参数,kw接收的是一个dict。

以及调用函数时如何传入可变参数和关键字参数的语法:

可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3))

关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{'a': 1, 'b': 2})

使用*args**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。

命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。

定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。

递归函数

1
2
3
4
5
# n!
def fact(n):
if n == 1:
return 1
return n * fact(n - 1)

Python并不支持尾递归优化,任何递归函数都存在栈溢出的问题。

高级特性

代码越少,开发效率越高。

切片

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
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']
>>> L[:3]
['Michael', 'Sarah', 'Tracy']
>>> L[1:3]
['Sarah', 'Tracy']
>>>
>>> L[-2:]
['Bob', 'Jack']
>>> L[-2:-1]
['Bob']
>>>
>>>
>>> L = list(range(100))
>>> L
[0, 1, 2, 3, ..., 99]
>>> L[:10] # 前10个
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> L[-10:] # 后10个
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
>>> L[10:20] # 前11-20个
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> L[:10:2] # 前10个 每2个取一个
[0, 2, 4, 6, 8]
>>> L[::5] # 所有数 每5个取一个
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
>>> L[:] # 原样复制
[0, 1, 2, 3, ..., 99]

tuple和str同样可以切片操作

1
2
3
4
5
6
>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)
>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'

迭代(Iteration)

在Python中,迭代是通过for ... in来完成的。

默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()

可通过collections模块的Iterable类型判断数据类型是否是可迭代对象。

1
2
3
4
5
6
7
>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False

Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C
>>>
>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
... print(x, y)
...
1 1
2 4
3 9

列表生成式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>>
>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>>
>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]
>>>
>>> [m + n for m in 'ABC' for n in 'XYZ'] # 全排列
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
>>>
>>> import os # 导入os模块
>>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录
['.emacs.d', '.ssh', '.Trash', 'Adlm', 'Applications', 'Desktop', 'Documents', 'Downloads', 'Library', 'Movies', 'Music', 'Pictures', 'Public', 'VirtualBox VMs', 'Workspace', 'XCode']
>>>
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']
>>>
>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']

生成器

通过列表生成式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。

如果列表元素可以按照某种算法推算出来,那是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。
在Python中,这种一边循环一边计算的机制,称为生成器:generator。

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
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> next(g)
81
>>> next(g) # Error StopIteration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> g = (x * x for x in range(10))
>>> for n in g:
... print(n)
...
0
1
4
9
16
25
36
49
64
81

使用函数实现列表生成器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 打印-斐波拉契数列
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'

# 斐波拉契数列-生成器
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
1
2
3
>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>
1
2
3
4
5
a, b = b, a + b
# 相当于
t = (b, a + b)
a = t[0]
b = t[1]
1
2
3
4
5
6
7
def odd():
print('step 1')
yield 1
print('step 2')
yield(3)
print('step 3')
yield(5)
1
2
3
4
5
6
7
8
9
10
11
>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o) # Error

生成器在执行过程中,遇到yield就会中断,下次调用又继续执行。

for循环过程中不断调用yield,就会不断中断。
要给循环设置一个条件来退出循环,不然会产出一个无限数列出来。

使用for循环调用generator时,发现拿不到generator的return语句返回值。
如果要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIterationvalue中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> g = fib(6)
>>> while True:
... try:
... x = next(g)
... print('g:', x)
... except StopIteration as e:
... print('Generator return value:', e.value)
... break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done

迭代器

可直接作用于for循环的数据类型:
1.集合数据类型,如listtupledictsetstr等;
2.generator,包括生成器和带yieldgenerator function

这些可以直接作用于for循环的对象统称为可迭代对象:Iterable

可以使用isinstance()判断一个对象是否是Iterable对象:

1
2
3
4
5
6
7
8
9
10
11
>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False

生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。

可以被next()函数调用并不断返回下一个值的对象称为:迭代器 Iterator

可以使用isinstance()判断一个对象是否是Iterator对象:

1
2
3
4
5
6
7
8
9
>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False

生成器都是Iterator对象。

listtupledictsetstr虽然是Iterable,却不是Iterator

iter()函数可以把Iterable变成Iterator:

1
2
3
4
5
>>> from collections import Iterator
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。
可以把这个数据流看做是一个有序序列,却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

凡是可作用于for循环的对象都是Iterable类型;
凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
集合数据类型是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。

Python的for循环本质上就是通过不断调用next()函数实现的,例如:

1
2
3
4
5
6
7
8
9
for x in [1, 2, 3, 4, 5]:
pass
# 完全等价于
it = iter([1, 2, 3, 4, 5]) # 获得Iterator对象
while True: # 循环
try:
x = next(it) # 获得下一个值
except StopIteration: # 遇到StopIteration就退出循环
break

函数式编程

通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。

函数式编程(请注意多了一个“式”字)—— Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

首先要搞明白计算机(Computer)和计算(Compute)的概念。

在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。

而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。

对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。

高阶函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> # 函数也是变量
>>> abs(-10)
10
>>> abs
<built-in function abs>
>>> f = abs
>>> f
<built-in function abs>
>>> f(-10)
10
>>>
>>>
>>> abs = 10
>>> abs(-10) # abs变量不再是函数 而指向了一个整数对象 重启恢复
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable

由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

1
2
def add(x, y, f):
return f(x) + f(y)
map/reduce

Python内建了map()reduce()函数。
Google论文MapReduce: Simplified Data Processing on Large Clusters

map函数接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

1
2
3
4
5
6
7
8
9
>>> def f(x):
... return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
>>>
>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']

reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算。

1
2
3
4
5
6
7
8
9
10
from functools import reduce

def str2int(s):
def fn(x, y):
return x * 10 + y

def char2num(s):
return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]

return reduce(fn, map(char2num, s))
1
2
3
4
5
6
7
from functools import reduce

def char2num(s):
return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]

def str2int(s): # lambda
return reduce(lambda x, y: x * 10 + y, map(char2num, s))
filter

filter把传入的函数依次作用于每个元素,然后根据返回值是(True保留 | False丢弃)该元素。

filter函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter完成计算结果,需要用list函数获得所有结果并返回list

1
2
3
4
5
6
7
8
9
10
11
12
>>> def is_odd(n):
... return n % 2 == 1
...
>>> list(filter(is_odd, range(16))) # 保留0-15奇数
[1, 3, 5, 7, 9, 11, 13, 15]
>>>
>>>
>>> def not_empty(s):
... return s and s.strip()
...
>>> list(filter(not_empty, ['A', 'B', '', None, 'C', ' '])) # 去空字符串
['A', 'B', 'C']

埃拉托斯特尼筛法计算素数。

首先,列出从2开始的所有自然数,构造一个序列:
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉:
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数5,然后用5把序列的5的倍数筛掉:
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
不断筛下去,就可以得到所有的素数。

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
# 一个生成从3开始的奇数的生成器
def _odd_iter():
n = 1
while True:
n = n + 2
yield n

# 过滤函数
def _not_divisible(n):
return lambda x: x % n > 0

# 不断返回素数的生成器
def primes():
yield 2
it = _odd_iter() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(_not_divisible(n), it) # 构造新序列

# 打印1000以内的素数:
for n in primes():
if n < 1000:
print(n)
else:
break
sorted

Python内置的sorted函数可以对list进行排序;
此外,sorted函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
>>>
>>> # 按照数的绝对值排序
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]
>>>
>>>
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']
>>>
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']
>>>
>>> # 反向排序
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']
1
2
3
4
5
6
7
8
9
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]

def by_name(t):
return t[0]
# 按照名字排序
print(sorted(L, key=by_name))

# 按成绩高低排序
print(sorted(L, key=lambda t:t[1], reverse=True))

返回函数

一个函数可以返回一个计算结果,也可以返回一个函数。

返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> def lazy_sum(*args):
... def sum():
... ax = 0
... for n in args:
... ax = ax + n
... return ax
... return sum
...
>>> f = lazy_sum(1,2,3)
>>> f
<function sum at 0x7ff0d0797578>
>>> f()
6
>>> # f1 和 f2 的调用结果互不影响
...
>>> f1 = lazy_sum(1,2,5,6,9)
>>> f2 = lazy_sum(1,2,5,6,9)
>>> f1 == f2
False
闭包

返回的函数在其定义内部引用了局部变量args,所以当一个函数返回了一个函数后,其内部的局部变量还被新函数引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> def count():
... fs = []
... for i in range(1, 4):
... def f():
... return i * i
... fs.append(f)
... return fs
...
>>> f1, f2, f3 = count()
>>>
>>> f1()
9
>>> f2()
9
>>> f3()
9

错误观点认为:f1()f2()f3()的结果应该为149
原因在于返回函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,i已经变成了3,因此结果为9

返回闭包时牢记一点:返回函数不要引用任何后续会发生变化的变量

以下方法可以“锁定”闭包中引用的变量(Like Erlang)。

1
2
3
4
5
6
7
8
9
10
11
def count():
def f(j):
def g():
return j * j
return g
fs = []
for i in range(1, 4):
# f(i)被立刻执行
# 因此i的当前值被传入了f()
fs.append(f(i))
return fs
1
2
3
4
5
6
7
>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9

可以利用 lambda函数 缩短代码:

1
2
3
4
5
6
7
8
9
def count():
def f(j):
return lambda : j * j
fs = []
for i in range(1, 4):
# f(i)被立刻执行
# 因此i的当前值被传入了f()
fs.append(f(i))
return fs

匿名函数

关键字lambda表示匿名函数,冒号前的部分表示函数参数。

lambda有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。

1
2
3
4
5
6
7
8
9
10
11
12
>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
>>>
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
>>> def build(x, y):
... return lambda: x * x + y * y
...
>>>

装饰器

函数也是一个对象,而且函数对象还可以被赋值给变量,所以,通过变量也能调用该函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> def now():
... print('2017-1-1')
...
>>> f = now
>>> f()
2017-1-1
>>>
>>> # 函数对象有个 __name__ 属性
... # 通过该属性可以取到函数名字
...
>>> now.__name__
'now'
>>> f.__name__
'now'
>>>
>>> f = lambda x: x
>>> f.__name__
'<lambda>'

假设现在需要增强now()函数的功能,比如:在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

本质上,Decorator就是一个返回函数的高阶函数。

1
2
3
4
5
6
7
8
9
10
def log(func):
def wrapper(*args, **kw):
print('call %s()' % func.__name__)
return func(*args, **kw)
return wrapper

# 执行 @log 等于执行了 now = log(now)
@log
def now():
print('2017-1-1')
1
2
3
>>> now()
call now()
2017-1-1

log是一个Decorator,返回一个函数。
原来的now函数仍然存在,只是现在的now变量指向了新的函数。
当调用now()将执行新函数,即在log函数中返回的wrapper函数。

wrapper函数的参数定义是(*args, **kw),因此,wrapper函数可以接受任意参数的调用。
wrapper函数内,首先打印日志,再调用原始函数。

如果Decorator本身需要传入参数,那就需要编写一个返回Decorator的高阶函数。
比如,要自定义log函数打印的文本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def log(text):
def dec(func):
def wrapper(*args, **kw):
print('%s %s()' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return dec

# 执行 @log('execute')
# 等于执行了
# now = log('execute')(now)
@log('execute')
def now():
print('2017-1-1')
1
2
3
>>> now()
execute now()
2017-1-1

当执行now()时,首先执行的是log('execute'),返回值是一个dec函数,再调用返回的dec函数,参数是now函数,返回值最终是wrapper函数。

1
2
>>> now.__name__
'wrapper'

经过Decorator装饰后,now函数的__name__属性已经从原来的'now'变成了'wrapper'

now.__name__的改变会导致有些依赖函数签名的代码执行出现错误。

Python内置的functools.wraps可以修正这种情况。

1
2
3
4
5
6
7
8
import functools

def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
1
2
3
4
5
6
7
8
9
10
import functools

def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator

通过import functools导入functools模块。
只需记住在定义wrapper函数的前面加上@functools.wraps(func)即可。

偏函数

Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> # int函数可以把字符串转换为整数
... # 默认十进制转换
...
>>> int('12345')
12345
>>> # 提供额外 base 参数
... # 修改默认进制
...
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565
>>> # 假设有大量二进制字符串需要转换为数字
... # 可以定义函数
...
>>> def int2(x, base=2):
... return int(x, base)
...
>>> # int2 就可以直接转换二进制了
...
>>> int2('1000000')
64
>>> int2('1010101')
85

通过使用functools.partial可以帮助创建一个偏函数,而不需要定义int2函数

1
2
3
4
5
6
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1000000', base=10)
1000000

简单总结functools.partial的作用就是:把一个函数的某些参数给固定住(设置默认值),返回一个新的函数。

创建偏函数时,实际上可以接收函数对象*args**kw这3个参数。

1
2
3
4
5
6
7
8
import functools
int2 = functools.partial(int, base=2)
# 给 int 函数的关键字参数 base 设置了默认值 2

int2('10010')
# 相当于
kw = {'base': 2}
int('10010', **kw)
1
2
3
4
5
6
7
8
import functools
max2 = functools.partial(max, 10)
# 10 作为 *args 的一部分自动加到左边

max2(5, 6, 7)
# 相当于
args = (10, 5, 6, 7)
max(*args)

模块

在Python中,一个.py文件就称之为一个模块Module
按目录来组织模块的方法,称为包Package

一个abc.py的文件就是一个名字叫abc的模块,一个xyz.py的文件就是一个名字叫xyz的模块。
假设abcxyz这两个模块名字与其他模块冲突了,可以通过包来组织模块,避免冲突。
方法是选择一个顶层包名,比如mypackage,按照如下目录存放:

1
2
3
4
mypackage/
├── __init__.py
├── abc.py
└── xyz.py

引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。
现在,abc.py模块的名字就变成了mypackage.abc,类似的,xyz.py的模块名变成了mypackage.xyz

注意:每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。
__init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是mypackage

类似的,可以有多级目录,组成多级层次的包结构。比如如下的目录结构:

1
2
3
4
5
6
7
8
9
mypackage/
├── web
│   ├── __init__.py # mypackage.web
│   ├── utils.py # mypackage.web.utils
│   └── www.py # mypackage.web.www
├── __init__.py # mypackage
├── abc.py # mypackage.abc
├── utils.py # mypackage.utils
└── xyz.py # mypackage.xyz

创建模块时要注意命名,不能和Python自带的模块名称冲突。例如,系统自带了sys模块,自己的模块就不可命名为sys.py,否则将无法导入系统自带的sys模块。

使用模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' a test module '

__author__ = 'Michael Liao'

# 导入 sys 模块
import sys

def test():
# sys.argv 使用list存储了命令行所有参数 sys.argv[0] == 'xx.py'
args = sys.argv
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')

# 特殊变量 __name__
# 当使用命令行执行本模块时,__name__ == '__main__'
if __name__=='__main__':
test()

模块正常的函数和变量名是公开的Public,可以被直接引用,如:abcx123PI等;
类似__xx__是特殊变量,可以被直接引用,但是有特殊用途,如:__name____author____doc__等;
类似_xx__xx是非公开的Private不应该被直接引用,如:_abc__abc等。

安装第三方模块

在Python中,安装第三方模块,是通过包管理工具pip完成的。
https://pypi.python.org

当试图加载一个模块时,Python会在sys.path指定的路径下搜索对应的.py文件,找不到就会报错。
可使用修改 sys.path设置 PYTHONPATH 环境变量两种方法添加搜索路径。

1
2
3
4
5
>>> # 修改 sys.path
...
>>> import sys
>>> sys.path.append('/usr/lib/python2.7')
>>>

面向对象编程

Object Oriented Programming是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。

面向过程的程序设计把计算机程序视为一系列命令的集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切割成子函数,从而降低系统的复杂度。

而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收并处理其它对象发过来的消息。程序的执行就是一系列消息在各个对象之间传递。

在Python中,所数据类型都可以视为对象,也可以自定义对象。
自定义的对象数据类型就是面向对象中的类Class的概念。

Class和实例Instance

1
2
3
4
5
# class 关键字
# Student 类名
# object 继承类,object 是最基础的
class Student(object):
pass
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> koko = Student()
>>> # koko 是Student类的实例
...
>>> koko
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>
>>>
>>> # 给实例变量绑定属性
...
>>> koko.name = 'koko'
>>> koko.name
'koko'

也可以在创建实例时,将必须的属性强制绑定上去:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student(object):
# 构造函数
def __init__(self, name, score):
self.name = name
self.score = score

# 析构函数
def __del__(self):
pass

# 类声明方法
def print_score(self):
print('name: %s; score: %s.' % (self.name, self.score))
1
2
3
4
5
6
7
8
9
>>> ko = Student('ko', 60)
>>> ko.score
60
>>> ko.score = 80
>>> ko.score
80
>>> ko.age # Error
>>> ko.print_score()
name: ko; score: 80.

类是创建实例的模板,实例是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;

方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;

通过在实例上调用方法,可直接操作了对象内部的数据,而无需知道方法内部的实现细节。

访问限制

如果要让类的内部属性不可以被外部访问,可以把属性的名称前加俩下划线__,这就变成了一个私有变量,只有内部可以访问,外部不能访问。

1
2
3
4
5
6
7
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score

def print_score(self):
print('name: %s; score: %s.' % (self.__name, self.__score))
1
2
3
4
5
6
7
8
9
10
>>> bob = Student('bob', 70)
>>> bob.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
>>> bob.print_score()
name: bob; score: 70.
>>>
>>> bob._Student__name
'bob'

虽然可以通过bob._Student__name获取私有变量__name,但强烈建议不要这么做,因为不同版本的Python解释器可能会把__name改成不同的变量名。

1
2
3
4
5
>>> bob.__name = 'ski'
>>> bob.__name
'ski'
>>> bob._Student__name
'bob'
变量命名 Public/Private 外部访问 Desc
abc Public Yes 公有变量
_abc Private Yes 可以被外部访问,但应该被视为私有变量,不要随意访问。
__abc Private No 私有变量
__abc__ Public Yes 特殊变量,具有特殊意义。

继承和多态

在OOP中,每当定义一个class时,可以从某个现有的class中继承,新的class称为子类Subclass,而被继承的class称为基类、父类或者超类(Base class, Super class)。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal(object):
def run(self):
print('Animal is running...')

class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')

class Cat(Animal):
def eat(self):
print('Eating fish...')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> animal = Animal()
>>> dog = Dog()
>>> cat = Cat()
>>>
>>> animal.run()
Animal is running...
>>> dog.run()
Dog is running...
>>> cat.run()
Animal is running...
>>>
>>> dog.eat()
Eating meat...
>>> cat.eat()
Eating fish...

当子类和父类都存在相同的run()方法时,子类的run()覆盖父类的run()。这样就是继承的另一个好处:多态。

当定义一个class的时候,实际上就是定义了一种数据类型。这种自定义的类型和Python自带的数据类型(str,list,dict)没什么两样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> a = list()
>>> b = Animal()
>>> c = Dog()
>>>
>>>
>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>>
>>> isinstance(c, Dog)
True
>>> isinstance(c, Animal)
True
>>>
>>> isinstance(b, Dog)
False

在继承关系中,如果一个实例的数据类型是某个子类,那么它的数据类型也可以被看做是父类。但是,反过来不行。

1
2
3
def run_twice(animal):
animal.run()
animal.run()
1
2
3
4
5
6
7
>>> run_twice(Animal())
Animal is running...
Animal is running...
>>>
>>> run_twice(Dog())
Dog is running...
Dog is running...
1
2
3
4
# 新增子类
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
1
2
3
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...

新增一个子类Tortoise,而不必对run_twice()做任何修改。
实际上,任何以Animal做为参数的函数或者方法都可以不加修改的正常运行,原因就在于多态

对于一个变量,只需要知道它是Animal类型,不需要知道确切的子类型,就可以放心地调用run()方法。
而调用run()方法时,执行的是Animal或者Dog或者Cat或者Tortoise中哪个run()方法则由运行时该对象的确切类型决定。

开闭原则:
对扩展开放(允许新增Animal子类);
对修改封闭(不需要修改依赖Animal类型的run_twice()函数)。

继承可以一级一级地继承下来。任何类,最终都可以追溯到根类object,继承关系像一棵树,object是树根。

鸭子类型

对于静态语言(例如Java),如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则将无法调用run()方法;
而对于动态语言(例如Python),则传入对象的不一定Animal类型或者它的子类,只需要保证传入的对象有run()方法就行了。

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那么它就可以被看做是鸭子。

Python的file-like object就是一种鸭子类型。
对于真正的文件对象,具有一个read()方法,返回其内容。
但是许多对象,只要含有read()方法,都可以被视为file-like object
许多函数接收的参数就是file-like object,不一定要传入真正的文件对象,只要实现了read()方法的对象都可以作为这些函数的参数。

获取对象信息

使用type函数判断对象类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
>>>
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>
>>>
>>> type(123) == type(456)
True
>>> type(123) == int
True
>>> type('abc') == type('123')
True
>>> type('abc') == str
True
>>> type('abc') == type(123)
False

使用types模块判断函数类型

1
2
3
4
5
6
7
8
9
10
11
12
>>> import types
>>> def fn():
... pass
...
>>> type(fn) == types.FunctionType
True
>>> type(abs) == types.BuiltinFunctionType
True
>>> type(lambda x: x) == types.LambdaType
True
>>> type(x for x in range(10)) == types.GeneratorType
True

使用isinstance判断变量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> d = Dog()
>>> isinstance(d, Dog)
True
>>> isinstance(d, Dog) and isinstance(d, 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

使用dir获得一个对象的所有属性和方法

1
2
>>> dir('ABC')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__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', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__返回长度。
在Python中,当调用len获取长度时,实际上len函数内部会自动去调用该对象是__len__方法。

1
2
3
4
5
6
>>> len('ABC')
3
>>> # 等价于
...
>>> 'ABC'.__len__()
3

自定义类可以通过写一个__len__方法,实现len函数

1
2
3
4
5
6
7
>>> class MyDog(object):
... def __len__(self):
... return 100
...
>>> dog = MyDog()
>>> len(dog)
100

通过getattr, setattr, hasattr操作一个对象状态

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
>>> class MyObject(object):
... def __init__(self):
... self.x = 9
... def power(self):
... return self.x * self.x
...
>>> obj = MyObject()
>>>
>>> hasattr(obj, 'x') # 判断是否含有 'x' 属性
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 判断是否含有 'y' 属性
False
>>> getattr(obj, 'y') # 获取不存在的属性 'y' AttributeError
>>>
>>> getattr(obj, 'y', 404) # 获取不存在的属性 'y' 返回默认值
404
>>> setattr(obj, 'y', 19) # 设置属性 'y'
>>> getattr(obj, 'y') # 获取属性 'y'
19
>>>
>>> hasattr(obj, 'power') # 判断是否含有 'power' 属性
True
>>> getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81

只有在不知道对象信息时,才需要去获取对象信息:
如果可以写成sum = obj.x + obj.y,就不要写成sum = getattr(obj, 'x') + getattr(obj, 'y')

正确的使用范例:

1
2
3
4
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None

当从文件流fp中读取图像,首先要判断该fp对象是否存在read方法。如果存在,则该对象是一个流;否则无法读取。
根据鸭子类型,有read方法并不能代表该fp对象就一定是一个文件流,也可以是网络流,或者内存中的一个字节流。但只要read方法返回的是有效的图像数据,就不影响读取图像的功能。

实例属性和类属性

Python是动态语言,根据类创建的实例可以绑定任意属性。

1
2
3
4
5
6
class Student(object):
# 类属性
career = 'Student'
def __init__(self, name):
# 实例属性
self.name = name
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> s = Student('puppy')
>>> print(s.career)
Student
>>> print(Student.career)
Student
>>> s.career = 'Worker'
>>> print(s.career)
Worker
>>> print(Student.career)
Student
>>> del s.career
>>> print(s.career)
Student

在开发过程中,千万不要将实例属性和类属性使用相同的名字,因为相同名字是实例属性将覆盖掉类属性,但当删除实例属性后,再使用相同的名字,访问到的将是类属性。

面向对象高级编程

当给一个自定义类创建实例之后,可以给该实例绑定任何属性和方法。

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
>>> class Student(object):
... pass
...
>>> s = Student()
>>> s.name = 'bob' # 动态给实例绑定一个属性
>>> print(s.name)
bob
>>> def set_age(self, age): # 定义一个实例的方法
... self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法
>>> s.set_age(25) #调用实例方法
>>> s.age
25
>>> # 给一个实例绑定方法,对另一个实例是不起作用的
...
>>> s2 = Student()
>>> s2.set_age(25)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'
>>> # 为了给所有实例都绑定该方法,需要对类进行方法绑定
...
>>> Student.set_age = set_age # 将类的一个变量绑定为一个函数
>>> Student.set_age = MethodType(set_age, Student) # 为类绑定一个方法
>>> s2.set_age(25)
>>> s2.age
25

动态绑定允许在程序运行过程中动态的给class添加功能,这在静态语言中很难实现。

__slots__

Python允许在定义类的时候,声明__slots__特殊变量,从而限制绑定实例的属性名称。

1
2
3
class Student(object):
# 用tuple定义允许绑定的属性名称
__slots__ = ('name', 'age')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> s = Student()
>>> s.name = 'bob' # accept
>>> s.age = 18 # accept
>>> s.score = 60
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
>>> # __slots__ 定义的属性仅对当前类的实例起作用,这个特殊变量是不被子类继承的
...
>>> class GraduateStudent(Student):
... pass
...
>>> g.GraduateStudent()
>>> g.score = 60
>>> g.score
60

除非在子类中也定义__slots__,这样子类实例允许定义的属性就是自身的__slots__和父类的__slots__的并集。

__slots__只能限制实例直接添加属性,并不能限制实例通过添加方法来添加属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> class Stu(object):
... __slots__ = ('name', 'set_age')
...
>>> s = Stu()
>>> s.name = 'bob'
>>> s.age = 18
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Stu' object has no attribute 'age'
>>>
>>> def set_age(self, age):
... self.age = age
...
>>> from types import MethodType
>>> Stu.set_age = MethodType(set_age, Stu)
>>> s.set_age(18)
>>> s.age
18

属性分实例属性和类属性,多个实例同时更改类属性,最后修改的值覆盖之前的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> class Stu(object):
... pass
...
>>>
>> def set_age(self, age):
... self.age=age
...
>>> from types import MethodType
>>> Stu.set_age = MethodType(set_age, Stu)
>>>
>>> a = Stu()
>>> b = Stu()
>>>
>>> a.set_age(10)
>>> b.set_age(15)
>>> print(a.age, b.age) # a和b自身没有age属性,访问的都是类属性age
15 15
>>> a.age = 18
>>> b.age = 20
>>>
>>> print(a.age, b.age)
18 20

@property

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student(object):

@property
def score(self):
return self._score

@score.setter
def score(self, score):
if not isinstance(score, int):
raise ValueError("score must be an integer!")
if score < 0 or score > 100:
raise ValueError("score must between 0~100!")
self._score = score
1
2
3
4
5
6
7
8
>>> s = Student()
>>> s.score = 60 # 实际转化为 s.set_score(60)
>>> s.score # 实际转化为 s.get_score()
60
>>> s.score = 998
Traceback (most recent call last):
...
ValueError: score must between 0~100!

在对实例属性进行操作时,通过@property,就可以不暴露原始属性,而是通过gettersetter方法来获取和设置属性值。

当只定义了getter方法,而不定义setter方法时,该属性实际上就是只读属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student(object):
# 可读可写属性 birth
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, birth):
self._birth = birth
# 只读属性 age
# age 可以通过 birth 计算出来
@property
def age(self):
return self._age

多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal(object):
pass

class Mammal(Animal):
pass

class Bird(Animal):
pass

class Runnable(object):
def run(self):
print('Running...')

class Flyable(object):
def fly(self):
print('Flying...')
1
2
3
4
5
class Dog(Mammal, Runnable):
pass

class Bat(Mammal, Flyable):
pass

通过多重继承,一个子类可以同时获得多个父类的所有功能。

MixIn

为了更好地看出继承关系,把RunnableFlyable改为RunnableMixInFlyableMixIn
类似的,还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn,让某个动物同时拥有好几个MixIn:

1
2
class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
pass

在设计类的时候,优先考虑通过多重继承来组合多个MixIn功能,而不是设计多层次的复杂的继承关系。

定制类

特殊命名 Var/Method Desc
__slots__ Var Tuple格式,限制类属性
__len__ Method 作用于len()函数
__str__ Method Format Print
__repr__ Method Format Debug Print
__iter__ Method 返回一个迭代对象
__next__ Method 拿到循环下一个值,StopIteration错误时退出循环
__getitem__ Method 类似于listordict,按照下标取出元素,参数可以是切片对象
__setitem__ Method 类似于listordict,为对象赋值
__delitem__ Method 类似于listordict,删除某元素
__getattr__ Method 默认情况下,当调用不存在的类属性会抛错,可通过添加该方法实现其它处理
__call__ Method 直接调用实例本身,可通过callable函数判断
#### 枚举类
1
2
3
4
5
6
from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)
1
2
3
4
5
6
7
8
9
10
11
12
from enum import Enum, unique

# 检查重复值
@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
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
>>> day1 = Weekday.Mon
>>> print(day1)
Weekday.Mon
>>> print(Weekday.Tue)
Weekday.Tue
>>> print(Weekday['Tue'])
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> print(day1 == Weekday.Mon)
True
>>> print(day1 == Weekday.Tue)
False
>>> print(Weekday(1))
Weekday.Mon
>>> print(day1 == Weekday(1))
True
>>> Weekday(7)
Traceback (most recent call last):
...
ValueError: 7 is not a valid Weekday
>>> for name, member in Weekday.__members__.items():
... print(name, '=>', member)
...
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat
#### 使用type函数定义class
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
1
2
3
4
5
6
7
8
9
10
11
>>> class Hello(object):
... def hello(self, name='world'):
... print('Hello %s.' % name)
...
>>> h = Hello()
>>> h.hello()
Hello world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>
1
2
3
4
5
6
7
8
9
10
11
>>> def fn(self, name='world'):
... print('Hello %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # Create Hello Class
>>> h = Hello()
>>> h.hello()
Hello world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

使用type函数创建class需要依次传入三个参数:
1.class的名称;
2.继承的父类集合,Python支持多重继承,如果只有一个父类,注意tuple单元素写法;
3.class的方法名称与函数绑定。

通过type函数创建的类和直接定义的class是完全一样的。
Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后直接调用type函数创建出class

metaclass

metaclass,直译为元类

当定义了类之后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。
连接起来就是:先定义metaclass,就可以创建类,最后创建实例。

按照约定,metaclass的类名总是以Metaclass结尾。

1
2
3
4
5
6
7
8
# metaclass是类的模板,所以必须从 type 类型派生:
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)

class MyList(list, metaclass=ListMetaclass):
pass

当传入关键字参数metaclass,Python解释器在创建MyList时,会通过ListMetaclass.__new__来创建。可以修改类的定义,比如增加新的方法,然后返回修改后的定义。

__new__方法接收到的参数依次是:
cls当前准备创建的类的对象
name类的名字
bases类继承的父类集合
attrs类的方法集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> # MyList可以调用add方法
...
>>> L = MyList()
>>> L.add(1)
>>> L
[1]
>>>
>>> # 而普通list没有add方法
...
>>> L2 = list()
>>> L2.add(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'

ORM

ORM全称”Object Relational Mapping”,即对象-关系映射
把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样写代码更简单,不用直接操作SQL语句。

要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
如果使用者在使用一个ORM框架,想定义一个User类来操作对应的数据库表User,只要写出这样的代码:

1
2
3
4
5
6
7
8
9
10
11
class User(Model):
# 定义类的属性到列的映射
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
passwd = StringField('passwd')

# 创建一个实例
u = User(id=123, name='Bob', email='test@orm.org', passwd='rootoor')
# 保存到数据库
u.save()

其中,父类Model和属性类型IntegerField,StringField都由ORM框架提供。save方法全部由metaclass自动完成。
虽然metaclass的编写会比较复杂,但ORM的使用者调用会异常简单。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 定义Field类 负责保存数据库表的字段名和字段类型
class Field(object):
def __init__(self, name, column_type):
self.name = name
self.column_type = column_type
def __str__(self):
return '<%s:%s>' % (self.__class__.__name__, self.name)

# 在Field的基础上 进一步定义各种类型的Field
class IntegerField(Field):
def __init__(self, name):
super(IntegerField, self).__init__(name, 'varchar(100)')

class StringField(Field):
def __init__(self, name):
super(StringField, self).__init__(name, 'bigint')

# 编写最复杂的ModelMetaclass
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
if name=='Model':
return type.__new__(cls, name, bases, attrs)
print('Found model: %s' % name)
mappings = dict()
for k, v in attrs.items():
if isinstance(v, Field):
print('Found mapping: %s ==> %s' % (k, v))
mappings[k] = v
for k in mappings.keys():
attrs.pop(k)
attrs['__mappings__'] = mappings
attrs['__table'] = name
return type.__new__(cls, name, bases, attrs)

# Model基类
class Model(dict, metaclass=ModelMetaclass):
def __init__(self, **kw):
super(Model, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Model' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
def save(self):
fields = []
params = []
args = []
for k, v in self.__mappings__.items():
fields.append(v.name)
params.append('?')
args.append(getattr(self, k, None))
sql = 'INSERT INTO %s (%s) VALUES (%s)' % (self.__table__, ','.join(fields), ','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))

当定义一个class User(Model)时,Python解释器首先在当前类User定义中查找metaclass,如果没有找到,就继续在父类Model中查找metaclass,找到后就使用Model中定义的metaclassModelMetaclass来创建User类。
也就是说:metaclass可以隐式地继承到子类,而子类本身却感觉不到。

ModelMetaclass中,实现了如下:
1.排除掉对Model类的修改;
2.在当前类(比如User)中查找定义的类的全部属性,如果找到Field属性,就把它保存到一个__mappings__dict中,同时从类属性中删除该Field属性,否则会造成Run-Time Error(实例的属性会遮盖类的同名属性);
3.把表名保存到__table__中。

Model中,就可以定义各种操作数据库的方法,比如save,delete,find,update等等。

1
2
3
4
5
6
7
8
9
>>> u = User(id=123, name='Bob', email='test@orm.org', passwd='rootoor')
>>> u.save()
Found model: User
Found mapping: email ==> <StringField:email>
Found mapping: passwd ==> <StringField:passwd>
Found mapping: id ==> <IntegerField:id>
Found mapping: name ==> <StringField:username>
SQL: INSERT INTO User (passwd,email,username,id) VALUES (?,?,?,?)
ARGS: ['rootoor', 'test@orm.org', 'Bob', 123]

错误、调试和测试

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
# try 出错
# except 执行
# finally 执行
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
1
2
3
4
try...
except: division by zero
finally...
END
1
2
3
4
5
6
7
8
9
10
11
# try 正常
# finally 执行
try:
print('try...')
r = 10 / 2
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
1
2
3
4
try...
result: 5
finally...
END
1
2
3
4
5
6
7
8
9
10
11
12
13
# 多个 except 语句
# 捕获不同类型的错误
try:
print('try...')
r = 10 / int('a')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
finally:
print('finally...')
print('END')
1
2
3
4
try...
ValueError: invalid literal for int() with base 10: 'a'
finally...
END
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 没有错误抛出时
# 执行 else 语句
try:
print('try...')
r = 10 / int('2')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
else:
print('no error!')
finally:
print('finally...')
print('END')
1
2
3
4
5
try...
result: 5.0
no error!
finally...
END

Python的错误实际上也是class,所有错误类型都是继承于BaseException
在使用except时,需要注意的是它不但捕获该类型的错误,还会捕获该类型子类型的错误。

1
2
3
4
5
6
7
8
try:
foo()
except ValueError as e:
print('ValueError')
# UnicodeError 永远不会被捕获
# 因为 UnicodeError 是 ValueError 的子类
except UnicodeError as e:
print('UnicodeError')

错误类型的继承关系:
https://docs.python.org/3/library/exceptions.html#exception-hierarchy

如果错误没有被捕获,它会沿着调用堆栈一直往上抛,直到最后被Python解释器捕获,打印出错误信息,然后程序退出。

如果不捕获错误,自然可以让Python解释器来打印出错误信息,但是程序也被终止了。
既然可以捕获错误,也就可以把错误信息打印出来的同时,让程序继续执行下去。
可以使用Python内置logging模块用来记录错误消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import logging

def foo(s):
return 10 / int(s)

def bar(s):
return foo(s) * 2

def main():
try:
bar('0')
except Exception as e:
logging.exception(e)

main()
print('END')
1
2
3
4
5
6
7
8
9
10
ERROR:root:division by zero
Traceback (most recent call last):
File "./t_logging.py", line 11, in main
bar('0')
File "./t_logging.py", line 7, in bar
return foo(s) * 2
File "./t_logging.py", line 4, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END

同样的抛错,但程序在打印错误消息后还会继续执行(‘END’被打印),并且正常退出。

1
2
3
4
5
6
7
8
9
10
11
# 自定义错误类型
class FooError(ValueError):
pass

def foo(s):
n = int(s)
if n == 0:
raise FooError('invalid value: %s' % s)
return 10 / n

foo('0')
1
2
3
4
5
6
Traceback (most recent call last):
File "./t_error.py", line 11, in <module>
foo('0')
File "./t_error.py", line 8, in foo
raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0

只有在必要的时候才需要定义自己的错误类型。
如果可以选择Python内置的错误类型,就尽量使用内置的错误类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def foo(s):
n = int(s)
if n == 0:
raise ValueError('invalid value: %s' % s)
return 10 / n

def bar():
try:
foo('0')
# 捕获后再次抛出
except ValueError as e:
print('Catch ValueError!')
raise

bar()
1
2
3
4
5
6
7
8
9
Catch ValueError!
Traceback (most recent call last):
File "./t_catch.py", line 15, in <module>
bar()
File "./t_catch.py", line 9, in bar
foo('0')
File "./t_catch.py", line 4, in foo
raise ValueError('invalid value: %s' % s)
ValueError: invalid value: 0
1
2
3
4
try:
10 / 0
except ZeroDivisionError:
raise ValueError('input error!')
1
2
3
4
5
6
7
8
9
10
11
Traceback (most recent call last):
File "t_catch.py", line 2, in <module>
10 / 0
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "t_catch.py", line 4, in <module>
raise ValueError('input error!')
ValueError: input error!

assert

在调试程序的时候,可以在代码中插入print函数,将变量打印出来。
还可以使用断言assert来代替print打印。

1
2
3
4
5
6
7
def foo(s):
n = int(s)
# 当断言为False时 程序抛出
assert n != 0, 'n is zero!'
return 10 / n

foo('0')
1
2
3
4
5
6
Traceback (most recent call last):
File "t_assert.py", line 7, in <module>
foo('0')
File "t_assert.py", line 4, in foo
assert n != 0, 'n is zero!'
AssertionError: n is zero!

如果断言失败,assert语句本身就会抛出AssertionError

启动Python解释器时,可以使用-O(大写字母O)参数来关闭assert

1
2
3
4
5
6
7
$ python -O t_assert.py
Traceback (most recent call last):
File "t_assert.py", line 6, in <module>
foo('0')
File "t_assert.py", line 4, in foo
return 10 / n
ZeroDivisionError: division by zero

加入-O参数后,所有assert都可以当成pass来看。

logging

assert相比,logging不会抛出错误,并且可以输出到文件。

logging可以指定记录信息的级别,有error,warning,info,debug等几个级别(日志等级从高到低):
当指定level=logging.INFO时,比info低的debug(logging.debug)就不起作用了;
同理当指定level=logging.WARNING后,infodebug就不起作用了。

logging还可以通过简单配置,使一条语句可以同时输出到不同的地方,比如Console和日志文件。

1
2
3
4
5
6
7
import logging
logging.basicConfig(level=logging.INFO)

s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
1
2
3
4
5
INFO:root:n = 0
Traceback (most recent call last):
File "t_logging.py", line 7, in <module>
print(10 / n)
ZeroDivisionError: division by zero

pdb Python Debugger

1
2
3
4
# t_pdb.py
s = '0'
n = int(s)
print(10 / n)

启动 pdb

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
$ python3 -m pdb t_pdb.py
> /mnt/d/test/py/t_pdb.py(2)<module>()
-> s = '0'
(Pdb) n
> /mnt/d/test/py/t_pdb.py(3)<module>()
-> n = int(s)
(Pdb) p s
'0'
(Pdb) h

Documented commands (type help <topic>):
========================================
EOF c d h list q rv undisplay
a cl debug help ll quit s unt
alias clear disable ignore longlist r source until
args commands display interact n restart step up
b condition down j next return tbreak w
break cont enable jump p retval u whatis
bt continue exit l pp run unalias where

Miscellaneous help topics:
==========================
exec pdb

(Pdb) q

输入n执行下一行;
输入p 变量名查看变量值;
输入h查看可使用命令;
输入q退出。

1
2
3
4
5
6
7
8
# t_pdb.py
import pdb

s = '0'
n = int(s)
# 设置断点
pdb.set_trace()
print(10 / n)
1
2
3
4
5
6
7
8
9
10
$ python3 t_pdb.py
> /mnt/d/test/py/t_pdb.py(8)<module>()
-> print(10 / n)
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):
File "t_pdb.py", line 8, in <module>
print(10 / n)
ZeroDivisionError: division by zero

程序运行到断点处会停止。
输入c继续运行。

IDE

使用IDE可以比较方便的进行断点、单步调试。
PyCharm
https://www.jetbrains.com/pycharm/

单元测试unittest

TDDTest-Driven Development测试驱动开发。

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

Python可以使用自带的unittest模块来进行单元测试。

1
2
3
4
5
6
7
8
9
10
11
# mydict.py
class Dict(dict):
def __init__(self, **kw):
super().__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
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
# mydict_test.py
import unittest

from mydict import Dict

# 编写测试类
# 从 unittest.TestCase 继承
class TestDict(unittest.TestCase):
# 每个测试方法都应该以 test 开头
def test_init(self):
d = Dict(a=1, b='test')
# 判断断言结果是否相等
self.assertEqual(d.a, 1)
self.assertEqual(d.b, 'test')
# 判断断言结果是否为True
self.assertTrue(isinstance(d, dict))
def test_key(self):
d = Dict()
d['key'] = 'value'
self.assertEqual(d.key, 'value')
def test_attr(self):
d = Dict()
d.key = 'value'
self.assertTrue('key' in d)
self.assertEqual(d['key'], 'value')
def test_keyerror(self):
d = Dict()
# 期望断言抛出指定的错误类型
# ‘期望 d['empty'] 抛出 KeyError’
with self.assertRaises(KeyError):
value = d['empty']
def test_attrerror(self):
d = Dict()
with self.assertRaises(AttributeError):
value = d.empty
1
2
3
4
5
6
7
# 运行方法一
# 在测试模块中添加如下代码
if __name__ == '__main__':
unittest.main()

# 然后在bash中直接运行Python脚本
# python3 mydict_test.py
1
2
3
4
5
6
7
8
$ # 运行方法二
$ # 在 bash 中 添加 -m unittest 参数
$ python3 -m unittest mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

在测试模块中可以添加两个特殊的方法setUptearDown
这两个方法会分别在调用每一个测试方法的前、后分别被执行。

1
2
3
4
5
6
7
import unittest

class TestDict(unittest.TestCase):
def setUp(self):
print('set up...')
def tearDown(self):
print('tear down...')

将这两个方法添加到上述测试模块会产生如下效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ python3 -m unittest mydict_test
set up...
tear down...
.set up...
tear down...
.set up...
tear down...
.set up...
tear down...
.set up...
tear down...
.
----------------------------------------------------------------------
Ran 5 tests in 0.009s

OK

1.单元测试可以有效地测试某个程序模块的行为;
2.单元测试的测试用例应该覆盖常用的输入组合、边界条件和异常;
3.单元测试代码要非常简单,不然无法保证测试代码本身不会有问题;
4.单元测试通过了并不意味着程序没有Bug,但是不通过一定有Bug。

文档测试doctest

Python内置的doctest模块可以直接提取注释中的代码并执行测试。

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
37
38
class Dict(dict):
'''
Simple dict but also support access as x.y style

>>> d1 = Dict()
>>> d1['x'] = 100
>>> d1.x
100
>>> d1.y = 200
>>> d1['y']
200
>>> d2 = Dict(a=1, b=2, c='3')
>>> d2.c
'3'
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'
>>> d2.empty
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
'''
def __init__(self, **kw):
super(Dict, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value

# 当模块正常导入时
# doctest不会被执行
if __name__ == '__main__':
import doctest
doctest.testmod()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ python3 t_doctest.py
$ # 没有任何输出表示测试通过了
$ # 屏蔽 __setattr__ 方法后再次执行
$ python3 t_doctest.py
**********************************************************************
File "t_doctest.py", line 10, in __main__.Dict
Failed example:
d1['y']
Exception raised:
Traceback (most recent call last):
File "/usr/lib/python3.4/doctest.py", line 1318, in __run
compileflags, 1), test.globs)
File "<doctest __main__.Dict[4]>", line 1, in <module>
d1['y']
KeyError: 'y'
**********************************************************************
1 items had failures:
1 of 9 in __main__.Dict
***Test Failed*** 1 failures.

doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。
只有测试异常的时候,可以用...表示中间一大段输出。

doctest非常有用,不但可以用来测试,还可以直接作为示例代码。
通过某些文档生成工具,可以自动把包含doctest的注释提取出来。

IO编程

IO在计算机中指Input/Output,也就是输入和输出。
由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。

IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。
Input Stream就是数据从外面(磁盘、网络)流进内存;
Output Stream就是数据从内存流到外面去。

由于CPU和内存的速度远远高于外设的速度,所以,在IO编程中,就存在速度严重不匹配的问题。
举个例子来说,比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒。
有两种办法:
第一种是CPU等着,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO
另一种方法是CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以立刻接着执行,这种模式称为异步IO

同步异步的区别就在于是否等待IO执行的结果。

好比你去麦当劳点餐,你说“来个汉堡”,服务员告诉你,对不起,汉堡要现做,需要等5分钟,于是你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO
你说“来个汉堡”,服务员告诉你,汉堡需要等5分钟,你可以先去逛商场,等做好了,我们再通知你,这样你可以立刻去干别的事情(逛商场),这是异步IO

很明显,使用异步IO来编写程序性能会远远高于同步IO
但是异步IO的缺点是编程模型复杂。想想看,你得知道什么时候通知你“汉堡做好了”,而通知你的方法也各不相同。
如果是服务员跑过来找到你,这是回调模式
如果服务员发短信通知你,你就得不停地检查手机,这是轮询模式

操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。

本章的IO编程都是同步模式。

文件读写

在磁盘上读写文件的功能都是由操作系统提供的。
现代操作系统不允许普通的程序直接操作磁盘。

读写文件就是请求操作系统打开一个文件对象(文件描述符),通过操作系统提供的接口从文件对象中读取数据或者把数据写入文件对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
try:
f = open('./demo.md', 'r')
print(f.read())
finally:
if f:
f.close()
# 由于读写文件都有可能产生 IOError
# 出错后,后面的 close 就不会被调用
# 可以使用 try ... finally 来确保文件会被关闭
# 或者使用Python的 with 语句
with open('./demo.py', 'r') as f:
print(f.read())
# 这等同于 try ... finally 方法

由于调用read()一次性读取文件的全部内容,如果文件很大有爆内存的风险,
保险起见,可以反复多次调用read(size)方法,每次最多读取size个字节的内容。

另外可以使用readline(),每次读取一行内容。
或者readlines(),一次读取所有内容并按行返回list(读取配置文件常用)。

1
2
3
for line in f.readlines():
# 去掉行尾的 '\n'
print(line.strip())

类似于open函数返回的具有read方法的对象都是file-like Object
除了文件外,还可以是内存中的字节流、网络流、自定义流等等。
file-like Object不要求从特定的类继承,只要写个read方法就行。

StringIO就是在内存中创建的file-like Object,常用作临时缓冲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 二进制读取
with open('./demo.jpg', 'rb') as f:
pass

# 采用 GBK 编码读取
with open('./gbk.txt', 'r', encoding='gbk') as f:
pass

# 采用 GBK 编码读取 并且 忽略文件中的编码错误
with open('./gbk.txt', 'r', encoding='gbk', errors='ignore') as f:
pass

# 写文本文件
with open('./demo.md', 'w') as f:
pass

# 写二进制文件
with open('./demo.jpg', 'wb') as f:
pass

StringIO

StringIO实现了在内存中读写str

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> from io import StringIO
>>> f = StringIO()
>>> f.write('hello')
5
>>> f.write(' ')
1
>>> f.write('world!')
6
>>> print(f.getvalue())
hello world!
>>>
>>> f = StringIO('Hello!\nHi!\nGoodbye!')
>>> while True:
... s = f.readline()
... if s == '':
... break
... print(s.strip())
...
Hello!
Hi!
Goodbye!

BytesIO

BytesIO实现了在内存中读写bytes

1
2
3
4
5
6
7
8
9
10
>>> from io import BytesIO
>>> f = BytesIO()
>>> f.write('中文'.encode('utf-8'))
6
>>> print(f.getvalue())
b'\xe4\xb8\xad\xe6\x96\x87'
>>>
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'

os

Python内置的os模块可以直接调用操作系统提供的接口函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> import os
>>> os.name
'posix'
>>> os.uname()
posix.uname_result(sysname='Linux', nodename='LAPTOP-7A1UQV9C', release='3.4.0+', version='#1 PREEMPT Thu Aug 1 17:06:05 CST 2013', machine='x86_64')
>>> os.environ.get('PATH')
'/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
>>>
>>> os.path.abspath('.')
'/mnt/d/test/py'
>>> os.mkdir('./testDir')
>>> os.listdir()
['js_log.py', 'learning.py', 'mydict.py', 'mydict_test.py', 't.py', 'testDir', '__pycache__']
>>> os.rmdir('testDir')
>>> os.listdir()
['js_log.py', 'learning.py', 'mydict.py', 'mydict_test.py', 't.py', '__pycache__']
>>>
>>> [x for x in os.listdir() if os.path.isfile(x) and os.path.splitext(x)[1] == '.py']
['js_log.py', 'learning.py', 'mydict.py', 'mydict_test.py', 't.py']

pickle

Python提供的pickle模块来实现序列化(Serialization)。

把变量从内存中变成可存储或传输的过程称之为序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> import pickle
>>> d = dict(name='Bob', age=20, score=88)
>>> pickle.dumps(d)
b'\x80\x03}q\x00(X\x05\x00\x00\x00scoreq\x01KXX\x03\x00\x00\x00ageq\x02K\x14X\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.'
>>> f = open('demo.b', 'wb')
>>> pickle.dump(d, f)
>>> f.close()
>>>
>>>
>>> f = open('demo.b', 'rb')
>>> d = pickle.load(f)
>>> f.close()
>>> d
{'score': 88, 'age': 20, 'name': 'Bob'}

json

JSON标准规定的JSON编码是UTF-8。
JSON表示的对象就是标准的JavaScript语言的对象。
JSON不仅是标准格式,并且比XML更快,而且可以直接在Web页面中读取。

JSON Pyton
{} dict
[] list
"string" str
123.456 int,float
true/false True/False
null None

Python内置的json模块提供了Python对象到JSON格式的转换。

1
2
3
4
5
6
7
8
>>> import json
>>> d = dict(name='Bob', age=20, score=88)
>>> jsonStr = json.dumps(d)
>>> jsonStr
'{"score": 88, "age": 20, "name": "Bob"}'
>>>
>>> json.loads(jsonStr)
{'score': 88, 'age': 20, 'name': 'Bob'}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> import json
>>> class Student(object):
... def __init__(self, name, age, score):
... self.name = name
... self.age = age
... self.score = score
...
>>> s = Student('Bob', 20, 88)
>>>
>>> s.__dict__
{'name': 'Bob', 'age': 20, 'score': 88}
>>>
>>> jsonStr = json.dumps(s, default=lambda obj: obj.__dict__)
>>>
>>> jsonStr
'{"name": "Bob", "age": 20, "score": 88}'

进程和线程

即使过去的单核CPU,也可以执行多任务。
由于CPU执行代码都是顺序执行的,操作系统可以轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。(时间片轮转调度法)
表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,用户感觉就像所有任务都在同时执行一样。

真正的并行执行多任务只能在多核CPU上实现。
由于任务数量远远多于CPU的核心数量,操作系统也会自动把很多任务轮流调度到每个核心上执行。

对于操作系统来说,一个任务就是一个进程(Process)。
在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,进程内的这些“子任务”称为线程(Thread)。
由于每个进程至少要干一件事,所以,一个进程至少有一个线程。

多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。
当然,真正地同时执行多线程需要多核CPU才可能实现。

前面编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。

如果要同时执行多个任务:
1.启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。(多进程模式)
2.启动一个进程,在一个进程内启动多个线程,多个线程也可以一块执行多个任务。(多线程模式)
3.启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,这种模型更复杂,实际很少采用。(多进程+多线程模式)

Python既支持多进程,又支持多线程。

线程是最小的执行单元,而进程由至少一个线程组成。
如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享。

多进程multiprocessing

os.fork

Linux/Unix操作系统提供了一个fork系统调用。
普通函数调用,调用一次,返回一次。
fork函数调用一次,返回两次,因为操作系统自动把当前进程(父进程)复制了一份(子进程),然后分别在父进程和子进程中返回。
子进程永远返回0,而父进程则返回子进程的进程ID。
一个父进程可以fork出很多子进程,所以,父进程要记录下每个子进程的进程ID。
而子进程只需要调用getppid就可以获取父进程的进程ID。

Python的os模块封装了常见的系统调用,其中就包括fork:

1
2
3
4
5
6
7
8
import os

print('Process (%s) start...' % os.getpid())
pid = os.fork()
if pid == 0:
print("I'm child process (%s) and parent is %s." % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
1
2
3
4
$ python3 ./t_fork.py
Process (14) start...
I (14) just created a child process (15).
I'm child process (15) and parent is 14.

有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。

multiprocessing.Process

由于Windows没有fork调用,Python内置的multiprocessing模块支持跨平台版本的多进程。
multiprocessing模块提供了一个Process类来代表一个进程对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
from multiprocessing import Process
import os

def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__ == '__main__':
print('Parent process %s.' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start()
p.join()
print('Child process end.')
1
2
3
4
5
$ python3 t_multiprocessing.py
Parent process 16.
Child process will start.
Run child process test (17)...
Child process end.

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start方法启动。
join方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

multiprocessing.Pool

如果要启动大量子进程,可以使用进程池的方式批量创建子进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from multiprocessing import Pool
import os, time, random

def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__ == '__main__':
print('Parent process %s.' % os.getpid())
# 默认参数是CPU的核数
p = Pool()
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocess done.')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python3 ./t_multiprocessing.py
Parent process 56.
Waiting for all subprocesses done...
Run task 0 (57)...
Run task 1 (58)...
Run task 2 (59)...
Run task 3 (60)...
Task 3 runs 1.31 seconds.
Run task 4 (60)...
Task 2 runs 1.76 seconds.
Task 0 runs 1.93 seconds.
Task 4 runs 0.90 seconds.
Task 1 runs 2.39 seconds.
All subprocess done.

Pool对象调用join方法会等待所有子进程执行完毕。
在调用join前必须调用close方法,调用close方法后就不能再添加新的Process了。

subprocess

使用subprocess模块启动一个子进程,然后控制其输入和输出。

1
2
3
4
5
import subprocess

print('START nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code: %s' % r)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ nslookup www.python.org
Server: 192.168.1.1
Address: 192.168.1.1#53

Non-authoritative answer:
www.python.org canonical name = python.map.fastly.net.
Name: python.map.fastly.net
Address: 151.101.88.223

$ python3 ./t_subprocess.py
START nslookup www.python.org
Server: 192.168.1.1
Address: 192.168.1.1#53

Non-authoritative answer:
www.python.org canonical name = python.map.fastly.net.
Name: python.map.fastly.net
Address: 151.101.88.223

Exit code: 0
1
2
3
4
5
6
7
import subprocess

print('START nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('utf-8'))
print('Exit code: ', p.returncode)
1
2
3
4
5
6
7
8
9
10
11
12
$ python3 ./t_subprocess.py
START nslookup
Server: 192.168.1.1
Address: 192.168.1.1#53

Non-authoritative answer:
python.org mail exchanger = 50 mail.python.org.

Authoritative answers can be found from:


Exit code: 0
multiprocessing.Queue
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
from multiprocessing import Process, Queue
import os, time, random

def write(q):
print('Process to write: %s' % os.getpid())
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())

def read(q):
print('Process to read: %s' % os.getpid())
while True:
value = q.get(True)
print('Get %s from queue...' % value)

if __name__ == '__main__':
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
pw.start()
pr.start()
# 等待写入Queue的进程结束
pw.join()
# 强制终止读取Queue的进程
pr.terminate()
1
2
3
4
5
6
7
8
9
$ python3 ./t_multiprocessing.py
Process to write: 126
Put A to queue...
Process to read: 127
Get A from queue...
Put B to queue...
Get B from queue...
Put C to queue...
Get C from queue...

进程间通信可以通过Queue,Pipes等实现。

由于Windows没有fork调用,因此,multiprocessing需要模拟出fork的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程中去。
所以,如果multiprocessing在Windows下调用失败了,首先要考虑是不是pickle失败了。

多线程

Python标准库提供了两个模块:_threadthreading
_thread是低级模块,threading是高级模块,对_thread进行了封装。
绝大多数情况下,只需要使用threading这个高级模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time, threading

def loop():
print('thread %s is running...' % threading.current_thread().name)
n = 0
while n < 5:
n = n + 1
print('thread %s >>> %s' % (threading.current_thread().name, n))
time.sleep(1)
print('thread %s ended.' % threading.current_thread().name)

print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)
1
2
3
4
5
6
7
8
9
10
$ python3 t_thread.py
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

由于任何进程都会默认启动一个子线程,该线程称为主线程。
threading.current_thread()会返回当前线程的实例。
主线程的名字叫MainThread,子线程的名字在创建时指定,如果没有指定,会默认命名为Thread-1, Thread-2...

由于多线程会共享所属进程内的所有变量,所以在修改内容时要加锁。

1
2
3
4
5
>>> import threading
>>> lock = threading.Lock() # 初始化
>>> lock.acquire() # 获取锁
True
>>> lock.release() # 释放锁
1
2
3
4
5
6
7
8
9
10
import threading

lock = threading.Lock()

lock.acquire()
try:
change()
finally:
# 确保锁一定会得到释放
lock.release()

如果存在多个锁,不同的线程持有不同的锁,并且试图获取对方持有的锁时,就可能造成“死锁”。
会导致多个线程全部阻塞,既不能执行,也无法结束,只能靠操作系统强制终止。

1
2
3
4
5
6
7
8
9
10
import threading, multiprocessing

def loop():
x = 0
while True:
x = x ^ 1

for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()

Python的死循环代码并不能把CPU核心直接跑满:
由于Python的线程虽然是真正的线程,但是Python解释器在执行代码时,有一个GIL锁:(Global Interpreter Lock)(每个Python进程持有一个),任何Python线程执行前,必须先获取GIL锁,然而每执行100条字节码,Python解释器会自动释放GIL锁,让别的线程有机会执行。
GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程运行在100核CPU上,也只能用到1个核。

所以,在Python中,多线程并不能有效利用多核。
但是Python可以通过多进程实现多核任务。
多个Python进程有各自独立的GIL锁,互不影响。

ThreadLocal

在多线程的环境下,一个线程使用自己的局部变量比使用全局变量号。
因为局部变量只有线程自己能看见,修改不会影响其它线程,而全局变量的修改必须加锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import threading

local_school = threading.local()

def process_student():
std = local_school.student
print('Hello %s (in %s)' % (std, threading.current_thread().name))

def process_thread(name):
local_school.student = name
process_student()

t1 = threading.Thread(target=process_thread, args=('Alice',), name='Thread-Alice')
t2 = threading.Thread(target=process_thread, args=('Bob',), name='Thread-Bob')
t1.start()
t2.start()
t1.join()
t2.join()
1
2
3
$ python3 t_ThreadLocal.py
Hello Alice (in Thread-Alice)
Hello Bob (in Thread-Bob)

一个ThreadLocal虽然是全局变量,但是每个线程只能读写自己线程的独立副本,互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
ThreadLocal解决了参数在一个线程中各个函数之间相互传递的问题。
ThreadLocal常用的地方就是为每个线程绑定一个数据库连接、HTTP请求、用户身份信息等。

进程 VS 线程

要实现多任务,通常会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,通常是一个Master,多个Worker

如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker

多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。

多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。

由于切换进程/线程是有代价的,无论是多进程还是多线程,只要数量一多,效率肯定上不去。

是否需要采用多任务可通过任务类型判断,任务类型可以分为计算密集型IO密集型:

计算密集型任务的特点是要进行大量的计算,消耗CPU资源。(计算圆周率、对视频进行高清解码等等)。
计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低。
所以,要最高效地利用CPU,同时进行的任务数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。
Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。
对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。
常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少。
因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。
对于IO密集型任务,最合适的语言就是开发效率最高的语言,脚本语言是首选,C语言最差。

现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO
如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型
Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。
由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。

对应到Python语言,单线程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。

分布式进程

threading模块相比,multiprocessing模块不仅仅支持通过多进程实现多任务,而且multiprocessing中的managers子模块还支持把多进程分布到多台机器上,而threading最多只能分布到同一台机器的多个CPU上。

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
# t_master.py
import random, time, queue
from multiprocessing.managers import BaseManager

task_queue = queue.Queue()
result_queue = queue.Queue()

class QueueManager(BaseManager):
pass

QueueManager.register('get_task_queue', callable=lambda: task_queue)
QueueManager.register('get_result_queue', callable=lambda: result_queue)

manager = QueueManager(address=('', 5000), authkey=b'secretKey')

manager.start()

task = manager.get_task_queue()
result = manager.get_result_queue()

for i in range(10):
n = random.randint(0, 100000)
print('Put task %d...' % n)
task.put(n)

print('Try get result...')
for i in range(10):
r = result.get(timeout=10)
print('Result: %s' % r)

manager.shutdown()
print('Master Exit.')
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
# t_worker.py
import time, sys, queue
from multiprocessing.managers import BaseManager

class QueueManager(BaseManager):
pass

QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')

server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
manager = QueueManager(address=(server_addr, 5000), authkey=b'secretKey')
manager.connect()

task = manager.get_task_queue()
result = manager.get_result_queue()

for i in range(10):
try:
n = task.get(timeout=1)
print('run task %d * %d...' % (n, n))
r = '%d * %d = %d' % (n, n, n*n)
time.sleep(1)
result.put(r)
except QueueEmpty:
print('task queue is empty.')
print('Worker Exit')

在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕过了QueueManager的封装,必须通过manager.get_task_queue()获得的Queue接口添加。

Queue之所以能通过网络访问,就是通过QueueManager实现的。
由于QueueManager管理的不止一个Queue,所以,要给每个Queue的网络调用接口起个名字,比如get_task_queue

authkey是为了保证两台机器正常通信,不被其他机器恶意干扰。

Queue的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小。
比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。

正则表达式

Python提供了re模块,包含了所有正则表达式的功能。

1
2
3
4
5
6
7
8
>>> s = 'ABC\\-001'
>>> # Python字符串 s
... # 对应的正则表达式字符串变成 'ABC\-001'
...
>>> s = r'ABC\\-001'
>>> # 添加 r 前缀的Python字符串
... # 对应的正则表达式字符串还是 'ABC\\-001'
...
1
2
3
4
5
>>> import re
>>> re.match(r'^[0-9]+$', '123') # 匹配成功返回 Match 对象
<_sre.SRE_Match object; span=(0, 3), match='123'>
>>> re.match(r'^[0-9]$', '123') # 匹配失败返回 None
>>>
1
2
3
4
5
6
7
>>> s = 'a  b c     d'
>>> s.split(' ')
['a', '', 'b', 'c', '', '', '', '', 'd']
>>> re.split(r' +', s)
['a', 'b', 'c', 'd']
>>> re.split(r' ', s)
['a', '', 'b', 'c', '', '', '', '', 'd']
1
2
3
4
5
6
7
8
9
10
11
12
>>> num = '021-123456'
>>> m = re.match(r'^([0-9]{3})-([0-9]{3,8})$', num)
>>> m
<_sre.SRE_Match object; span=(0, 10), match='021-123456'>
>>> m.group(0)
'021-123456'
>>> m.group(1)
'021'
>>> m.group(2)
'123456'
>>> m.groups()
('021', '123456')

正则默认使用贪婪匹配,也就是匹配尽可能多的字符。

1
2
3
4
>>> re.match(r'^([0-9]+)(0*)$', '102300').groups()
('102300', '')
>>> re.match(r'^([0-9]+?)(0*)$', '102300').groups()
('1023', '00')

如果一个正则表达式要重复使用几千次,出于效率的考虑,可以预编译该正则表达式:

1
2
3
4
5
6
7
>>> re_phone = re.compile(r'^([0-9]{3})-([0-9]{3,8})$')
>>> re_phone
re.compile('^([0-9]{3})-([0-9]{3,8})$')
>>> re_phone.match('010-12399').groups()
('010', '12399')
>>> re_phone.match('100-8666').groups()
('100', '8666')

常用内建模块Batteries Included

datetime

Python内置datetime处理日期和时间。

1
2
3
4
5
6
7
8
9
10
11
12
>>> from datetime import datetime
>>>
>>> now = datetime.now()
>>> now
datetime.datetime(2017, 6, 28, 15, 2, 9, 809854)
>>> print(now)
2017-06-28 15:02:09.809854
>>> type(now)
<class 'datetime.datetime'>
>>> dt = datetime(2011,2,3,4,5,6)
>>> print(dt)
2011-02-03 04:05:06
1
2
3
4
5
6
7
>>> import datetime
>>>
>>> now = datetime.datetime.now()
>>> now
datetime.datetime(2017, 6, 28, 15, 4, 55, 955948)
>>> print(now)
2017-06-28 15:04:55.955948

在计算机中,时间是用数字表示的。
1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time,记为0(1970年以前的时间timestamp为负数),当前时间就是相对于epoch time的秒数,称为timestamp

1
2
3
timestamp = 0 = 1970-1-1 00:00:00 UTC+0:00
# 对应的北京时间是:
timestamp = 0 = 1970-1-1 08:00:00 UTC+8:00

** timestamp的值与时区毫无关系,因为timestamp一旦确定,其UTC时间就确定了,转换到任意时区的时间也是完全确定的,这就是为什么计算机存储的当前时间是以timestamp表示的,因为全球各地的计算机在任意时刻的timestamp都是完全相同的(假定时间已校准)。 **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> from datetime import datetime
>>>
>>> now = datetime.now()
>>> now.timestamp()
1498634687.391587
>>> # Python的timestamp是一个浮点数,小数位表示毫秒数
... # 某些编程语言(如Java和JavaScript)的timestamp使用整数表示毫秒数
... # 这种情况下只需要把timestamp除以1000就得到Python的浮点表示方法
...
>>> nowTimestamp = datetime.now().timestamp()
>>> nowTimestamp
1498636659.663946
>>> now = datetime.fromtimestamp(nowTimestamp)
>>> now
datetime.datetime(2017, 6, 28, 15, 57, 39, 663945)
>>> print(now)
2017-06-28 15:57:39.663945

** timestamp没有时区的概念,而datetime是有时区的。 **

1
2
3
4
5
6
>>> from datetime import datetime
>>> t = 1429417200.0
>>> print(datetime.fromtimestamp(t)) # 本地时间(UTC+8:00)北京时间
2015-04-19 12:20:00
>>> print(datetime.utcfromtimestamp(t)) # UTC标准时间(UTC+0:00)格林威治标准时间
2015-04-19 04:20:00

通过datetime.strptime方法可以把字符串转换成datetime

1
2
3
4
5
6
7
>>> from datetime import datetime
>>>
>>> cday = datetime.strptime('2015-6-1 18:19:59', '%Y-%m-%d %H:%M:%S')
>>> cday
datetime.datetime(2015, 6, 1, 18, 19, 59)
>>> print(cday)
2015-06-01 18:19:59

https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior

通过datetime.strftime方法可以把datetime转换成字符串。

1
2
3
4
5
6
7
8
9
10
>>> from datetime import datetime
>>>
>>> now = datetime.now()
>>> now
datetime.datetime(2017, 6, 28, 17, 14, 12, 85059)
>>> nowStr = now.strftime('%a, %b %d %H:%M')
>>> nowStr
'Wed, Jun 28 17:14'
>>> print(nowStr)
Wed, Jun 28 17:14

对日期和时间进行加减实际上就是把datetime往后或往前计算,得到新的datetime
加减可以直接使用+-运算符,不过需要先导入timedelta这个类。

1
2
3
4
5
6
7
8
9
10
>>> from datetime import datetime, timedelta
>>> now = datetime.now()
>>> now
datetime.datetime(2017, 7, 10, 15, 23, 16, 743616)
>>> now + timedelta(hours=10)
datetime.datetime(2017, 7, 11, 1, 23, 16, 743616)
>>> now - timedelta(days=1)
datetime.datetime(2017, 7, 9, 15, 23, 16, 743616)
>>> now + timedelta(days=2, hours=12)
datetime.datetime(2017, 7, 13, 3, 23, 16, 743616)

本地时间是指系统设定时区的时间,例如北京时间是UTC+8:00时区,而UTC时间是指UTC+0:00时区的时间。

一个datetime类型有一个时区属性tzinfo,默认为None,所以无法区分这个datetime到底是哪个时区,除非强行给datetime设置一个时区。

1
2
3
4
5
6
7
8
9
10
>>> from datetime import datetime, timedelta, timezone
>>> tz_utc_8 = timezone(timedelta(hours=8))
>>> tz_utc_8
datetime.timezone(datetime.timedelta(0, 28800))
>>> now = datetime.now()
>>> now
datetime.datetime(2017, 7, 10, 15, 27, 51, 513948)
>>> dt = now.replace(tzinfo=tz_utc_8)
>>> dt
datetime.datetime(2017, 7, 10, 15, 27, 51, 513948, tzinfo=datetime.timezone(datetime.timedelta(0, 28800)))

如果系统时区恰好是UTC+8:00,那么上述代码就是正确的,否则,不能强制设置为UTF+8:00时区的时间。

可以先通过utcnow()拿到当前的UTC时间,再转换为任意时区的时间。

1
2
3
4
5
6
7
8
9
10
>>> print(datetime.now())
2017-07-10 15:33:43.156632
>>> print(datetime.utcnow())
2017-07-10 07:33:53.637040
>>> utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
>>> print(utc_dt)
2017-07-10 07:34:24.405083+00:00
>>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
>>> print(bj_dt)
2017-07-10 15:34:24.405083+08:00

时区转换的关键在于:拿到一个datetime时,要获知其正确的时区,然后强制设置时区,作为基准时间。

利用带时区的datetime,通过astimezone方法,可以转换到任意时区。

datetime表示的时间需要时区信息才能确定一个特定的时间,否则只能视为本地时间。

如果要存储datetime,最佳的方法是将其转换为timestamp再存储,因为timestamp的值与时区完全无关。

collections

函数namedtuple可以用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> point = (1, 2)
>>> point
(1, 2)
>>
>>> from collections import namedtuple
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(1, 2)
>>> p.x
1
>>> p.y
2
>>> p
Point(x=1, y=2)
>>>
>>> isinstance(p, Point)
True
>>> isinstance(p, tuple)
True

使用list存储数据时,按索引访问元素很快,但是插入和删除元素都很慢。
因为list是线性存储,数据量大的时候,插入和删除效率很低。

deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> from collections import deque
>>> q = deque(['a', 'b', 'c'])
>>> q.append('x')
>>> q.appendleft('y')
>>> q
deque(['y', 'a', 'b', 'c', 'x'])
>>> q.pop()
'x'
>>> q
deque(['y', 'a', 'b', 'c'])
>>> q.popleft()
'y'
>>> q
deque(['a', 'b', 'c'])

使用dict时,如果引用的Key不存在,就会抛出KeyError
使用defaultdict可以在访问不存在的Key时,返回一个默认值。

1
2
3
4
5
6
7
>>> from collections import defaultdict
>>> dd = defaultdict(lambda: 'N/A')
>>> dd['key'] = 'abc'
>>> dd['key']
'abc'
>>> dd['keykey']
'N/A'

使用dict时,Key是无序的。
在对dict做迭代时,无法确定Key的顺序。
如果要保持Key的顺序,可以使用OrderedDict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> from collections import OrderedDict
>>>
>>> d = dict([('a', 1), ('b', 2), ('c', 3)])
>>> d
{'c': 3, 'a': 1, 'b': 2}
>>>
>>> od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
>>> od
OrderedDict([('a', 1), ('b', 2), ('c', 3)])
>>>
>>> # OrderedDict的Key会按照插入的顺序排列
...
>>> od = OrderedDict()
>>> od['z'] = 1
>>> od['y'] = 2
>>> od['x'] = 3
>>> list(od.keys())
['z', 'y', 'x']

可以使用OrderedDict实现一个FIFOdict,当容量超出限制时,先删除最早添加的Key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# t_fifo_dict.py
from collections import OrderedDict

class LastUpdatedOrderedDict(OrderedDict):
def __init__(self, capacity):
super(LastUpdatedOrderedDict, self).__init__()
self._capacity = capacity

def __setitem__(self, key, value):
containsKey = 1 if key in self else 0
if len(self) - containsKey >= self._capacity:
last = self.popitem(last=False)
print('remove:', last)
if containsKey:
del self[key]
print('set:', (key, value))
else:
print('add:', (key, value))
OrderedDict.__setitem__(self, key, value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> from t_fifo_dict import *
>>>
>>> d = LastUpdatedOrderedDict(capacity=3)
>>> d
LastUpdatedOrderedDict()
>>> d['a'] = 1
add: ('a', 1)
>>> d
LastUpdatedOrderedDict([('a', 1)])
>>> d['b'] = 2
add: ('b', 2)
>>> d
LastUpdatedOrderedDict([('a', 1), ('b', 2)])
>>> d['c'] = 3
add: ('c', 3)
>>> d
LastUpdatedOrderedDict([('a', 1), ('b', 2), ('c', 3)])
>>> d['d'] = 4
remove: ('a', 1)
add: ('d', 4)
>>> d
LastUpdatedOrderedDict([('b', 2), ('c', 3), ('d', 4)])

可以使用Counter实现一个简单的字符统计,Counter实际上就是一个dict的子类。

1
2
3
4
5
6
7
>>> from collections import Counter
>>> c = Counter()
>>> for ch in 'programming':
... c[ch] = c[ch] + 1
...
>>> c
Counter({'r': 2, 'm': 2, 'g': 2, 'a': 1, 'p': 1, 'n': 1, 'o': 1, 'i': 1})

base64

Base64是一种用64个字符来表示任意二进制数据的方法,是一种最常见的二进制编码方法。
Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%。
如果要编码的二进制数据不是3的倍数,Base64用\x00字节在末尾补足后,再在编码的末尾加上1个或2个=等号,表示补了多少字节,解码的时候,会自动去掉。

1
2
3
4
5
>>> import base64
>>> base64.b64encode(b'binary-binary-binary')
b'YmluYXJ5LWJpbmFyeS1iaW5hcnk='
>>> base64.b64decode(b'YmluYXJ5LWJpbmFyeS1iaW5hcnk=')
b'binary-binary-binary'

由于标准的Base64编码后可能出现字符+/,在URL中就不能直接作为参数,所以又有一种url safe的Base64编码,会把字符+/分别变成-_

1
2
3
4
5
6
7
>>> import base64
>>> base64.b64encode(b'i\xb7\x1d\xfb\xef\xffa')
b'abcd++//=='
>>> base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xffa')
b'abcd--__=='
>>> base64.urlsafe_b64decode('abcd--__==')
b'i\xb7\x1d\xfb\xef\xffa'

Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。
Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等。

由于=字符也可能出现在Base64编码中,但=用在URL、Cookie里面会造成歧义,所以,很多Base64编码后会把=去掉:
去掉=后怎么解码呢?因为Base64是把3个字节变为4个字节,所以,Base64编码的长度永远是4的倍数,因此,需要加上=把Base64字符串的长度变为4的倍数,就可以正常解码了。
base64.b64decode(s + b'=' * (4 - len(s) % 4))

struct

Python没有专门处理字节的数据类型,b'str'可表示字节。
Python提供struct模块来处理bytes和其它二进制数据类型的转换。

1
2
3
4
5
6
>>> import struct
>>> struct.pack('>I', 10241024)
b'\x00\x9cD\x00'
>>>
>>> struct.unpack('>IH', b'\x00\x9cD\x00\x04\x00')
(10241024, 1024)

参数'>I'中的>表示字节顺序是big-endian,也就是网络序;I表示4字节无符号整数。
参数'>IH'中的H表示2字节无符号整数。

https://docs.python.org/3/library/struct.html#format-characters

Python不适合编写底层操作字节流的代码。

hashlib

Python的hashlib提供了常见的摘要算法,如MD5、SHA1等等。
摘要算法又称哈希算法、散列算法。它可以把任意长度的数据转换为一个长度固定的数据串(通常是16进制的字符串)。
摘要算法通过摘要函数f()对任意长度的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被篡改过。
摘要函数是一个单向函数,计算f(data)很容易,而通过digest反推data却非常困难,而且对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> import hashlib
>>> md5 = hashlib.md5()
>>> md5
<md5 HASH object @ 0x7fd509544b20>
>>> md5.update('who are u?'.encode('utf-8'))
>>> md5.hexdigest()
'a36783e5d4a3c782b05198ac8a1ac5c3'
>>>
>>> md5 = hashlib.md5()
>>> md5
<md5 HASH object @ 0x7fd508e44170>
>>> md5.update('who a'.encode('utf-8'))
>>> md5.update('re u?'.encode('utf-8'))
>>> md5.hexdigest()
'a36783e5d4a3c782b05198ac8a1ac5c3'

MD5摘要算法速度很快,生成结果通常是一个32位的16进制字符串。

1
2
3
4
5
6
7
8
>>> import hashlib
>>> sha1 = hashlib.sha1()
>>> sha1
<sha1 HASH object @ 0x7f0b35ca8620>
>>> sha1.update('who a'.encode('utf-8'))
>>> sha1.update('re u?'.encode('utf-8'))
>>> sha1.hexdigest()
'6f10cdf9244e7086be930c361b60c759bfdb04ee'

SHA1生成的结果通常是一个40位的16进制字符串。
比SHA1更安全的算法有SHA256和SHA512,但是摘要长度更长,生成速度也更慢。

** 任何摘要算法都是把无限多的数据集合映射到一个有限的集合中,有可能两个不同的数据确计算出相同的摘要,这种情况称为碰撞。 **

摘要算法通常用于在数据库中存储用户的密码:
一般不会直接在数据库中以明文的形式存储用户密码,而采用存储用户密码的摘要的方式,当用户登陆时,会先计算用户输入的明文密码的摘要,再和数据库存储的密码摘要对比,判断是否一致。

当黑客获取了数据库中存储的用户密码摘要时,不会采用费时费力的反推明文密码,而是会通过黑客事先计算的常用密码和摘要建立的反推表,来反推简单的密码,所以简单的密码很不安全。

可以通过“加盐”的方式使密码更加安全,在用户创建密码时,并不简单的存储密码的摘要,而是存储get_md5(passwd + 'The-Salt'),这样会使简单密码的摘要也不可被黑客的反推表破解。

itertools

Python内建模块itertools提供了用于操作迭代对象的函数。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
>>> import itertools
>>> natuals = itertools.count(1)
>>> for n in natuals:
... print(n)
...
1
2
3
^C
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyboardInterrupt
>>>
>>> cs = itertools.cycle('ABC')
>>> for c in cs:
... print(c)
...
A
B
C
A
B
^C
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyboardInterrupt
>>>
>>> ns = itertools.repeat('A', 3)
>>> for n in ns:
... print(n)
...
A
A
A
>>>
>>> natuals = itertools.count(1)
>>> ns = itertools.takewhile(lambda x: x<= 10, natuals)
>>> list(ns)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>>
>>> for c in itertools.chain('AB', 'CD'):
... print(c)
...
A
B
C
D
>>>
>>> for key, group in itertools.groupby('AAABBBCCAAA'):
... print(key, list(group))
...
A ['A', 'A', 'A']
B ['B', 'B', 'B']
C ['C', 'C']
A ['A', 'A', 'A']
>>>
>>> for key, group in itertools.groupby('AaaBBbcCAAa', lambda c: c.upper()):
... print(key, list(group))
...
A ['A', 'a', 'a']
B ['B', 'B', 'b']
C ['c', 'C']
A ['A', 'A', 'a']

Python中itertools模块提供的全部是处理迭代功能的函数,它们的返回值并不是list,而是Iterator,只有用for循环迭代的时候才真正的计算惰性计算

contextlib

1
2
3
4
5
6
7
8
9
10
try:
f = open('/path/file', 'r')
r.read()
finally:
if f:
f.close()
# 可使用 try...finally... 确保文件会被关闭
# 也可以使用 with 语句
with open('/path/file', 'f') as f:
f.read()

并不只有open函数返回的fp对象才能使用with语句。
只要实现了上下文管理,就可以使用with语句。
实现上下文管理是通过__enter____exit__这两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# t_with.py
class Query(object):
def __init__(self, name):
self.name = name

def __enter__(self):
print('Begin')
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
print('Error')
else:
print('End')

def query(self):
print('Query info about %s...' % self.name)
1
2
3
4
5
6
7
8
>>> from t_with import *
>>> with Query('Bob') as q:
... q.query()
...
Begin
Query info about Bob...
End
>>>

通过实现__enter____exit__来使用with语句依然很繁琐。

Python标准库提供的contextlib有更简单的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# t_with.py
from contextlib import contextmanager

class Query(object):
def __init__(self, name):
self.name = name

def query(self):
print('Query info about %s...' % self.name)

@contextmanager
def create_query(name):
print('Begin')
q = Query(name)
# 用 yield 语句把 with... as var 的变量输出去
yield q
print('End')
1
2
3
4
5
6
7
8
>>> from t_with import *
>>> with create_query('Bob') as q:
... q.query()
...
Begin
Query info about Bob...
End
>>>

也可以用@contextmanager实现在某段代码前后自动执行特定的代码:

1
2
3
4
5
6
7
8
# t_with.py
from contextlib import contextmanager

@contextmanager
def tag(name):
print('<%s>' % name)
yield
print('</%s>' % name)
1
2
3
4
5
6
7
8
9
10
>>> from t_with import *
>>> with tag('h1'):
... print('Hello')
... print('World')
...
<h1>
Hello
World
</h1>
>>>

代码执行的顺序是:
1.with语句首先执行yield之前的语句,因此打印出<h1>
2.yield调用会执行with语句内部的所有语句,因此打印出HelloWorld
3.最后执行yield之后的语句,打印出</h1>

如果一个对象没有实现上下文管理,就不能把它用于with语句,这个时候,可用closing方法把该对象变为上下文对象

1
2
3
4
5
6
7
8
from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('https://www.python.org')) as page:
lineNum = 0
for line in page:
lineNum = lineNum + 1
print(lineNum)

其实closing也是一个经过@contextmanager装饰的generator,它的作用就是把任意对象变为上下文对象

1
2
3
4
5
6
@contextmanager
def closing(thing):
try:
yield thing
finally:
thing.close()

XML

XML虽然比JSON复杂,并且在Web中应用也不如以前多了,不过仍然有很多地方在用。

操作XML有两种方法:
1.DOM会把整个XML读入内存,解析成树,因此占用内存大,解析慢,优点是可以任意遍历树的节点
2.SAX是流模式,占用内存少,解析快,缺点是需要开发者自己处理事件

正常情况下,优先考虑使用SAX

Python中使用SAX解析XML时,通常要关心的事件是start_elementend_elementchar_data

SAX解析器读到一个XML节点时:
<a href="/">python</a>
会产生3个事件,其中:
1.start_element事件在读取<a href="/">
2.char_data事件在读取python
3.end_element事件在读取</a>

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
# t_xml.py
from xml.parsers.expat import ParserCreate

class DefaultSaxHandler(object):
def start_element(self, name, attrs):
print('SAX: start_element: %s, attrs: %s' % (name, str(attrs)))

def end_element(self, name):
print('SAX: end_element: %s' % name)

def char_data(self, text):
print('SAX: char_data: %s' % text)

xml = r'''<?xml version="1.0"?>
<ol>
<li><a href="/python">Python</a></li>
<li><a href="/ruby">Ruby</a></li>
</ol>
'''

handler = DefaultSaxHandler()
parser = ParserCreate()
parser.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data
parser.Parse(xml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ python3 t_xml.py
SAX: start_element: ol, attrs: {}
SAX: char_data:

SAX: char_data:
SAX: start_element: li, attrs: {}
SAX: start_element: a, attrs: {'href': '/python'}
SAX: char_data: Python
SAX: end_element: a
SAX: end_element: li
SAX: char_data:

SAX: char_data:
SAX: start_element: li, attrs: {}
SAX: start_element: a, attrs: {'href': '/ruby'}
SAX: char_data: Ruby
SAX: end_element: a
SAX: end_element: li
SAX: char_data:

SAX: end_element: ol

当注意的是读取一大段字符串时,CharacterDataHandler可能被多次调用,所以需要先保存起来,然后在EndElementHandler里面再合并。

** 如果需要生成复杂的XML时,考虑改用JSON。 **

HTMLParser

HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或者SAX来解析HTML。

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
37
# t_html.py
from html.parser import HTMLParser
from html.entities import name2codepoint

class MyHTMLParser(HTMLParser):
def handle_starttag(self, tag, attrs):
print('<%s>' % tag)

def handle_endtag(self, tag):
print('</%s>' % tag)

def handle_startendtag(self, tag, attrs):
print('<%s/>' % tag)

def handle_data(self, data):
print(data)

def handle_comment(self, data):
print('<!--', data, '-->')

def handle_entityref(self, name):
print('&%s:' % name)

def handle_charref(self, name):
print('&#%s' % name)

html = r'''<html>
<head></head>
<body>
<!-- test html parser -->
<p>Some <a href=\"#\">HTML</a> HTML&nbsp;tutorial...<br/>END</p>
</body>
</html>
'''

parser = MyHTMLParser()
parser.feed(html)
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
$ python3 t_html.py
<html>


<head>
</head>


<body>


<!-- test html parser -->


<p>
Some
<a>
HTML
</a>
HTML
&nbsp:
tutorial...
<br/>
END
</p>


</body>


</html>


$

其实并不需要一次性把整个HTML字符串都塞进去,feed方法可以多次调用,这样可以一部分一部分塞进去。

HTML的特殊字符有两种表示方法:
1.命名实体,类似于&nbsp;
2.十进制编码,类似于&#160;

这两种表示方法都可以通过Parser解析出来。

urllib

Python的urllib提供了一系列用于操作URL的功能。

urllib中的request模块可以抓取URL中的内容,也就是发送一个GET请求到指定的页面,然后返回HTTP响应。

1
2
3
4
5
6
7
8
9
# t_urllib.py
from urllib import request

with request.urlopen('https://api.douban.com/v2/book/2129650') as f:
data = f.read()
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', data.decode('utf-8'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ python3 t_urllib.py
Status: 200 OK
Date: Thu, 13 Jul 2017 07:24:30 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2055
Connection: close
Vary: Accept-Encoding
X-Ratelimit-Remaining2: 97
X-Ratelimit-Limit2: 100
Expires: Sun, 1 Jan 2006 01:00:00 GMT
Pragma: no-cache
Cache-Control: must-revalidate, no-cache, private
Set-Cookie: bid=JPMxvW_6rro; Expires=Fri, 13-Jul-18 07:24:30 GMT; Domain=.douban.com; Path=/
X-DOUBAN-NEWBID: JPMxvW_6rro
X-DAE-Node: nain1
X-DAE-App: book
Server: dae
Data: {"rating":{"max":10,"numRaters":16,"average":"7.4","min":0},"subtitle":"","author":["廖雪峰编著"],......

通过往Request对象添加HTTP头,可以把请求伪装成浏览器。
伪装的方法是先监控浏览器发出的请求,再根据浏览器的请求头来伪装,User-Agent头就是用来标识浏览器的。
例如,模拟iPhone6去请求豆瓣首页:

1
2
3
4
5
6
7
8
9
from urllib import request

req = request.Request('http://www.douban.com/')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone: CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
with request.urlopen(req) as f:
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', f.read().decode('utf-8'))

如果要以POST发送一个请求,只需要把参数databytes形式传入。
例如,模拟微博登陆:

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
from urllib import request, parse

print('Login to weibo.cn...')
email = input('Email: ')
passwd = input('Password: ')
data = [
('username', email)
,('password', passwd)
,('entry', 'mweibo')
,('client_id', '')
,('savestate', '1')
,('ec', '')
,('pagerefer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')
]
login_data = parse.urlencode(data)

req = request.Request('https://passport.weibo.cn/sso/login')
req.add_header('Orign', 'https://passport.weibo.cn')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone: CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
req.add_header('Referer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')

with request.urlopen(req, data=login_data.encode('utf-8')) as f:
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', f.read().decode('utf-8'))

如果需要通过Proxy去访问网站,可以利用ProxyHandler来处理。

1
2
3
4
5
6
proxy_handler = urllib.request.ProxyHandler({'http': 'http://www.example.com:3128/'})
proxy_auth_handler = urllib.request.ProxyBasicAuthHandler()
proxy_auth_handler.add_password('realm', 'host', 'username', 'password')
opener = urllib.request.build_opener(proxy_handler, proxy_auth_handler)
with opener.open('http://www.example.com/login.html') as f:
pass

PIL(Python Imaging Library)

PIL已经是Python平台上图像处理标准库了。
由于PIL仅支持到Python2.7,志愿者在PIL的基础上创建了兼容的版本,名字叫Pillow,支持最新Python3.X。
$ pip install pillow
生成字母验证码图片:

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
from PIL import Image, ImageDraw, ImageFont, ImageFilter

import random

def rndChar():
return chr(random.randint(65, 90))

def rndColorB():
return (random.randint(64, 255), random.randint(64, 255), random.randint(64, 255))

def rndColorT():
return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127))

width = 60 * 4
height = 60
image = Image.new('RGB', (width, height), (255, 255, 255))
# Font对象
font = ImageFont.truetype('arial.ttf', 36)
# Draw对象
draw = ImageDraw.Draw(image)
# 填充背景
for x in range(width):
for y in range(height):
draw.point((x, y), fill=rndColorB())
# 填充文字
for t in range(4):
draw.text((60 * t + 10, 10), rndChar(), font=font, fill=rndColorT())
# 模糊
image = image.filter(ImageFilter.BLUR)
image.save('demo.jpg', 'jpeg')

virtualenv

在开发Python应用的时候,当Python的版本是3.4时,所有第三方的包都会被pip安装到Python3的site-packages目录下。
如果要同时开发多个应用,那这些应用都会共用一个Python。

如果应用A需要jinja 2.7,而应用B需要jinja 2.6怎么办?
这种情况下,每个应用可能需要各自拥有一套“独立”的Python运行环境。
virtualenv就是用来为每一个应用创建一套“隔离”的Python运行环境。
pip install virtualenv

假定是要开发一个新的项目,需要一套独立的Python运行环境,可以这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ # 1.创建目录
$ mkdir myproject
$ cd myproject/
$ # 2.创建一个独立的Python运行环境 命名为venv
$ virtualenv --no-site-packages venv
Using base prefix '/usr/local/.../Python.framework/Versions/3.4'
Installing setuptools, pip, wheel...done.
$ # 3.使用source进入该环境
$ source venv/bin/activate
(venv)$
(venv)$ # 4.退出虚拟环境
(venv)$ deactivate
$

进入虚拟环境后,命令提示符会有个(venv)前缀,表示当前环境是一个名为venv的Python环境。

当用命令source venv/bin/activate进入一个virtualenv环境时,会把系统Python复制一份到virtualenv的环境,virtualenv会修改相关环境变量,让命令pythonpip均指向当前的virtualenv环境。

图形界面

Python支持多种GUI编程的第三方库,包括:Tk、wxWidgets、Qt、GTK…

Python自带的库支持Tk的Tkinter,无需安装任何包,直接就可以使用。

Python代码会调用内置的Tkinter,Tkinter封装了访问Tk的接口;
Tk是一个图形库,使用Tcl语言开发,并且支持多个操作系统;
Tk会调用操作系统提供的本地GUI接口,完成最终的GUI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from tkinter import *

class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.createWidgets()

def createWidgets(self):
# 创建一个Label
self.helloLabel = Label(self, text='Hello World')
self.helloLabel.pack()
# 创建一个Button 被点击后触发 self.quit()
self.quitButton = Button(self, text='Quit', command=self.quit)
self.quitButton.pack()

# 实例化
app = Application()
# 设置窗口标题
app.master.title('Hello World')
# 主消息循环
app.mainloop()

在GUI中,每个Button、Label、输入框等,都是一个Widget。
Frame则是可以容纳其它Widget的Widget。
所有的Widget组合起来就是一棵树。

pack方法把Widget加入到父容器中,并实现布局。
pack是最简单的布局,可以使用grid实现更复杂的布局。

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
from tkinter import *
import tkinter.messagebox as messagebox

class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.createWidgets()

def createWidgets(self):
self.nameInput = Entry(self)
self.nameInput.pack()
# 点击按钮后触发 self.hello()
self.alertButton = Button(self, text='Hello', command=self.hello)
self.alertButton.pack()

def hello(self):
# 获取用户输入的文本
name = self.nameInput.get() or 'world'
# 弹出消息对话框
messagebox.showinfo('Message', 'Hello, %s' % name)

app = Application()
app.master.title('Hello World')
app.mainloop()

Python内置的Tkinter可以满足基本GUI程序的要求,如果要非常复杂的GUI程序,建议采用操作系统原生支持的语言和库来编写。

网络编程

计算机网络就是把各个计算机连接到一起,让网络中的计算机可以互相通信。
网络编程就是如何在程序中实现两台计算机的通信。
** 网络通信本质上是两台计算机上的两个进程之间的通信。 **
网络编程对所有开发语言都是一样的,Python也不例外。
用Python进行网络编程,就是在Python程序本身这个进程内,连接别的服务器进程的通信端口进行通信。

TCP/IP

为了把所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,互联网协议簇(Internet Protocol Suite)就是通用协议标准。

Internet是由”inter-net”两个单词组合起来的,原意就是“连接-网络”的网络,有了Internet,任何私有网络,只要支持这个协议,就可以联入互联网。

互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,把互联网的协议简称TCP/IP协议。

通信的时候,双方必须知道对方的标识。
互联网上每个计算机的唯一标识就是IP地址,类似123.123.123.123。

如果一台计算机同时接入到两个或更多的网络,比如路由器,它就会有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。
IP协议负责把数据从一台计算机通过网络发送到另一台计算机。
数据被分割成一小块一小块,然后通过IP包发送出去。
由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。

IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。

IPv4地址实际上是一个32位整数,以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。
IPv6地址实际上是一个128位整数,是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334。

TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。
TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。

一个IP包除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。

在两台计算机通信时,只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个IP包来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。
每个网络程序都向操作系统申请唯一的端口号,这样,两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。
一个进程也可能同时与多个计算机建立链接,因此它会申请很多端口。

TCP

Socket是网络编程中的一个抽象概念,通常用一个Socket表示“打开了一个网络链接”。
打开一个Socket需要知道目标计算机的IP地址和端口号,并且指定一个协议类型。

大多数连接都是可靠的TCP连接。
创建TCP时,由客户端主动发起连接,服务端被动响应。

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
# Client
import socket

# 创建一个Socket
# AF_INET 指使用IPv4协议
# AF_INET6 指使用IPv6协议
# SOCK_STREAM 指面向流的TCP协议
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接
# 参数是一个tuple 包括地址和端口号
# 端口号小于1024是Internet标准服务的端口:SMTP-25、FTP-21...
s.connect(('github.com', 80))
# 连接建立后 就可以发送数据了
# TCP连接创建的是双向通道,双方都可以同时给对方发送数据。如何协调由具体协议来决定
s.send(b'GET / HTTP/1.1\r\nHost: github.com\r\nConnection: close\r\n\r\n')

#接收数据
buffer = []
# 当recv返回空数据时 代表接收结束 退出循环
while True:
# 每次最多接收1k字节
# recv(max)限制一次接收指定的字节数
d = s.recv(1024)
if d:
buffer.append(d)
else:
break
data = b''.join(buffer)
# 关闭Socket
s.close()

header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
with open('./github.html', 'wb') as f:
f.write(html)

服务端进程首先要绑定一个端口并监听来自其它客户端的连接。
由于服务端会有大量来自客户端的连接,所以,服务端要能够区分一个Socket连接是和哪个客户端绑定的。
一个Socket依赖4项来确定一个Socket:
1.Server IP
2.Server Port
3.Client IP
4.Client Port

由于服务端还要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理。

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
# Server
import socket, threading

# 创建一个Socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定监听地址和端口
# 0.0.0.0 绑定到所有网络地址
# 127.0.0.1 绑定到本机地址(外部无法访问)
s.bind(('0.0.0.0', 9999))
# 监听端口
# 等待连接的最大数量为5
s.listen(5)
# 通过永久循环来接受来自客户端的连接
# accept会阻塞并返回一个客户端的连接
while True:
sock, addr = s.accept()
# 创建新的线程来处理TCP连接
t = threading.Thread(target=tcplink, args=(sock, addr))
t.start()

# 每个连接都必须创建新线程(进程)来处理
# 否则,单线程在处理连接的过程中,无法处理其他客户端的连接

def tcplink(sock, addr):
print('Accept new connection from %s:%s...' % addr)
sock.send(b'Welcome!')
while True:
data = sock.recv(1024)
time.sleep(1)
if not data or data.decode('utf-8') == 'exit':
break
sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
sock.close()
print('Connection from %s:%s closed.' % addr)

用TCP协议进行Socket编程在Python中十分简单:
对于客户端,要主动连接服务器的IP和指定端口,
对于服务端,要首先监听指定端口,对每一个新的连接,创建一个线程或进程来处理。

通常,服务端程序会无限运行下去。

UDP

TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。

UDP则是面向无连接的协议。
使用UDP协议时,不需要建立连接,只需要知道对方的IP和Port,就可以发送数据包,但是能不能到达就不知道了。

虽然UDP传输数据不可靠,但是比TCP速度快。
对于不要求可靠到达的数据,就可以使用UDP协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Server
import socket

# SOCK_DGRAM 指定Socket类型是UDP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定地址和端口
s.bind(('0.0.0.0', 9999))
# 但是UDP不需要调用 listen 方法
# 直接接收来自任何客户端的数据
while True:
data, addr = s.recvfrom(1024)
print("Received from %s:%s." % addr)
s.sendto(b'Hello %s' % data, addr)
1
2
3
4
5
6
7
8
9
10
11
# Client
import socket

# SOCK_DGRAM 指定Socket类型是UDP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 客户端使用UDP 不需要调用 connect
# 直接通过 sendto 发送
for data in [b'PA', b'PB', b'PC']:
s.sendto(data, ('127.0.0.1', 9999))
print(s.recv(1024).decode('utf-8'))
s.close()

** 服务器绑定UDP端口和TCP端口互不冲突,也就是:UDP的9999端口和TCP的9999端口可以各自绑定。 **

电子邮件

电子邮件软件被称为MUAMail User Agent邮件用户代理。

Email从MUA发出去,不是直接到达对方电脑,而是发到MTAMail Transfer Agent邮件传输代理,就是那些Email服务提供商,比如网易、新浪等等。

假设使用网易的邮箱,发送给新浪的邮箱:
Email首先被投递到网易提供的MTA,再由网易的MTA发到对方服务商,也就是新浪的MTA。这个过程中间可能还会经过别的MTA,但是不用关心具体路线。
Email到达新浪的MTA后,新浪的MTA会把Email投递到邮件的最终目的地MDAMail Delivery Agent邮件投递代理。
Email到达MDA后,就静静地躺在新浪的某个服务器上,存放在某个文件或特殊的数据库里,通常将这个长期保存邮件的地方称之为电子邮箱。
Email不会直接到达对方的电脑,因为对方电脑不一定开机,开机也不一定联网。对方要取到邮件,必须通过MUA从MDA上把邮件取到自己的电脑上。

所以,一封电子邮件的旅程就是:
** 发件人 -> MUA -> MTA -> 若干个MTA -> MTA -> MDA <- MUA <- 收件人 **

有了上述基本概念,要编写程序来发送和接收邮件,本质上就是:
1.编写MUA把邮件发到MTA;
2.编写MUA从MDA上收邮件。

发邮件时,MUA和MTA使用的协议就是SMTPSimple Mail Transfer Protocol,MTA到另一个MTA也是用SMTP协议。

收邮件时,MUA和MDA使用的协议有两种:
1.POPPost Office Protocol,目前版本是3,俗称POP3;
2.IMAPInternet Message Access Protocol,目前版本是4,优点是不但能取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱移到垃圾箱,等等。

邮件客户端软件在发邮件时,先需要配置SMTP服务器,也就是要发到哪个MTA上。假设正在使用网易的邮箱,就不能直接发到新浪的MTA上,因为它只服务新浪的用户,所以,得填网易提供的SMTP服务器地址:smtp.163.com。

类似的,从MDA收邮件时,邮件客户端软件也会要求POP3或IMAP服务器地址,这样,MUA才能顺利地通过POP或IMAP协议从MDA取到邮件。

SMTP发送邮件

SMTP是发送邮件的协议。
Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件、带附件的邮件。

Python对SMTP的支持有smtplibemail两个模块,email负责构造邮件,smtplib负责发送邮件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1.构造纯文本邮件
from email.mime.text import MIMEText
# 第一个参数是邮件正文
# 第二个参数是MIME的subtype plain表示纯文本 最终MINE就是 text/plain
# 第三个参数是编码规范
msg = MIMEText('hello send by Python...', 'plain', 'utf-8')
# 2.发送邮件
import smtplib
from_addr = input('From: ')
passwd = input('Passwd: ')
to_addr = input('To: ')
smtp_server = input('SMTP Server: ')
# SMTP协议默认端口是25
server = smtplib.SMTP(smtp_server, 25)
# Level-1会打印出和SMTP服务器交互的所有信息
server.set_debuglevel(1)
# 登陆SMTP服务器
server.login(from_addr, passwd)
# 第二个参数是list 可以同时发送给多人
# 邮件正文是一个str as_string把MIMEText对象变成str
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import parseaddr, formataddr

import smtplib

def _format_addr(s):
name, addr = parseaddr(s)
# 如果包含中文
# 一定要使用 Header 来编码
return formataddr((Header(name, 'utf-8').encode(), addr))

from_addr = input('From: ')
passwd = input('Passwd: ')
to_addr = input('To: ')
smtp_server = input('SMTP Server: ')

# --- 构造HTML邮件
# html = '''<html>
# <body>
# <h1>Hellp</h1>
# </body>
# </html>
# '''
# htmlMsg = MIMEText(html, 'html', 'utf-8')

# --- 添加附件
# # 邮件对象
# msg = MIMEMultipart()
# # 邮件正文是MIMEText:
# msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))
# # 添加附件就是加上一个MIMEBase,从本地读取一个图片:
# with open('./test.png', 'rb') as f:
# # 设置附件的MIME和文件名,这里是png类型:
# mime = MIMEBase('image', 'png', filename='test.png')
# # 加上必要的头信息:
# mime.add_header('Content-Disposition', 'attachment', filename='test.png')
# mime.add_header('Content-ID', '<0>')
# mime.add_header('X-Attachment-Id', '0')
# # 把附件的内容读进来:
# mime.set_payload(f.read())
# # 用Base64编码:
# encoders.encode_base64(mime)
# # 添加到MIMEMultipart:
# msg.attach(mime)

# --- 将图片添加到邮件正文
# # 要先把图片添加到附件里,然后根据图片的cid引用
# addImage = '''<html>
# <body>
# <h1>Hello</h1>
# <p><img src="cid:0"></p>
# </body>
# <html>
# '''
# msg.attach(MIMEText(addImage, 'html', 'utf-8'))

msg = MIMEText('Hello, send by Python...', 'plain', 'utf-8')
msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)
# 接收字符串而不是list
# 如果有多个,可在字符串中用逗号分隔
msg['To'] = _format_addr('管理员 <%s>' % to_addr)
msg['Subject'] = Header('来自SMTP的问候:-D', 'utf-8').encode()

server = smtplib.SMTP(smtp_server, 25)
server.set_debuglevel(1)
server.login(from_addr, passwd)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 发送HTML格式的邮件时 可以再添加一个纯文本邮件
# 这样当接收者无法查看HTML邮件时 就可以自动降级查看纯文本邮件

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

msg = MIMEMultipart('alternative')
msg['From'] = ...
msg['To'] = ...
msg['Subject'] = ...

msg.attach(MIMEText('hello', 'plain', 'utf-8'))
msg.attach(MIMEText('<html><body><h1>Hello</h1></body><html>', 'html', 'utf-8'))
`
1
2
3
4
5
6
7
8
9
import smtplib

smtp_server = 'smtp.gmail.com'
smtp_port = 587
# 加密SMTP
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()

server.set_debuglevel(1)

https://docs.python.org/3/library/email.mime.html

POP3收取邮件

收取邮件就是编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或者手机上。
收取邮件最常用的协议是POP协议,目前版本号是3,俗称POP3。

Python内置一个poplib模块,实现了POP3协议,可以直接用来收邮件。

POP3协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP协议很像,SMTP发送的也是经过编码后的一大段文本。

要把POP3收取的文本变成可以阅读的邮件,还需要用email模块提供的各种类来解析原始文本,变成可阅读的邮件对象。

所以,收取邮件分两步:
1.用poplib把邮件的原始文本下载到本地;
2.用email解析原始文本,还原为邮件对象。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# --- 通过POP3下载邮件
import poplib

# 输入邮件地址, 口令和POP3服务器地址:
email = input('Email: ')
password = input('Password: ')
pop3_server = input('POP3 server: ')

# 连接到POP3服务器:
server = poplib.POP3(pop3_server)
# 可以打开或关闭调试信息:
server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字:
print(server.getwelcome().decode('utf-8'))

# 身份认证:
server.user(email)
server.pass_(password)

# stat()返回邮件数量和占用空间:
print('Messages: %s. Size: %s' % server.stat())
# list()返回所有邮件的编号:
resp, mails, octets = server.list()
# 可以查看返回的列表类似[b'1 82923', b'2 2184', ...]
print(mails)

# 获取最新一封邮件, 注意索引号从1开始:
index = len(mails)
resp, lines, octets = server.retr(index)
# 假如要获取所有邮件,只需要循环使用retr()

# lines存储了邮件的原始文本的每一行,
# 可以获得整个邮件的原始文本:
msg_content = b'\r\n'.join(lines).decode('utf-8')
# 稍后解析出邮件:
msg = Parser().parsestr(msg_content)

# 可以根据邮件索引号直接从服务器删除邮件:
# server.dele(index)
# 关闭连接:
server.quit()

# --- 解析邮件
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr

# 把邮件内容解析为Message对象
msg = Parser().parsestr(msg_content)
# Message对象本身可能是一个MIMEMultipart对象,即包含嵌套的其他MIMEBase对象,嵌套可能还不止一层。
# 所以要递归地打印出Message对象的层次结构

# indent用于缩进显示:
def print_info(msg, indent=0):
if indent == 0:
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header=='Subject':
value = decode_str(value)
else:
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr)
print('%s%s: %s' % (' ' * indent, header, value))
if (msg.is_multipart()):
parts = msg.get_payload()
for n, part in enumerate(parts):
print('%spart %s' % (' ' * indent, n))
print('%s--------------------' % (' ' * indent))
print_info(part, indent + 1)
else:
content_type = msg.get_content_type()
if content_type=='text/plain' or content_type=='textml':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
print('%sText: %s' % (' ' * indent, content + '...'))
else:
print('%sAttachment: %s' % (' ' * indent, content_type))
# 邮件的Subject或者Email中包含的名字都是经过编码后的str,要正常显示,就必须decode
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value

# decode_header()返回一个list
# 因为像Cc、Bcc这样的字段可能包含多个邮件地址,所以解析出来的会有多个元素。
# 上面的代码偷了个懒,只取了第一个元素。

# 文本邮件的内容也是str,还需要检测编码,否则,非UTF-8编码的邮件都无法正常显示:
def guess_charset(msg):
charset = msg.get_charset()
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset

访问数据库

目前广泛使用的关系数据库也就这么几种:
00.付费的商用数据库:
01.Oracle,典型的高富帅;
02.SQL Server,微软自家产品,Windows定制专款;
03.DB2,IBM的产品,听起来挺高端;
04.Sybase,曾经跟微软是好基友,后来关系破裂,现在家境惨淡。
10.免费的开源数据库:
11.MySQL,大家都在用,一般错不了;
12.PostgreSQL,学术气息有点重,其实挺不错,但知名度没有MySQL高;
13.sqlite,嵌入式数据库,适合桌面和移动应用。

SQLite

SQLite是一种嵌入式数据库,它的数据库就是一个文件。
由于SQLite本身是C写的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在iOS和Android的App中都可以集成。

Python就内置了SQLite3,所以,在Python中使用SQLite,不需要安装任何东西,直接使用。

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
37
# 导入SQLite驱动:
>>> import sqlite3
# 连接到SQLite数据库
# 数据库文件是test.db
# 如果文件不存在,会自动在当前目录创建:
>>> conn = sqlite3.connect('test.db')
# 创建一个Cursor:
>>> cursor = conn.cursor()
# 执行一条SQL语句,创建user表:
>>> cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')
<sqlite3.Cursor object at 0x10f8aa260>
# 继续执行一条SQL语句,插入一条记录:
>>> cursor.execute('insert into user (id, name) values (\'1\', \'Michael\')')
<sqlite3.Cursor object at 0x10f8aa260>
# 通过rowcount获得插入的行数:
>>> cursor.rowcount
1
# 关闭Cursor:
>>> cursor.close()
# 提交事务:
>>> conn.commit()
# 关闭Connection:
>>> conn.close()
>>>
# 查询记录:
>>> conn = sqlite3.connect('test.db')
>>> cursor = conn.cursor()
# 执行查询语句:
# 使用?作为占位符
>>> cursor.execute('select * from user where id=?', ('1',))
<sqlite3.Cursor object at 0x10f8aa340>
# 获得查询结果集:
>>> values = cursor.fetchall()
>>> values
[('1', 'Michael')]
>>> cursor.close()
>>> conn.close()

使用Python的DB-API时,只要搞清楚Connection(连接)和Cursor(游标)对象,打开后一定记得关闭。

MySQL

MySQL是Web世界中使用最广泛的数据库服务器。
SQLite的特点是轻量级、可嵌入,但不能承受高并发访问,适合桌面和移动应用。
而MySQL是为服务器端设计的数据库,能承受高并发访问,同时占用的内存也远远大于SQLite。

MySQL内部有多种数据库引擎,最常用的引擎是支持数据库事务的InnoDB。

在Windows上,安装时请选择UTF-8编码,以便正确地处理中文。
在Mac或Linux上,需要编辑MySQL的配置文件,把数据库默认的编码全部改为UTF-8。

MySQL的配置文件默认存放在/etc/my.cnf或者/etc/mysql/my.cnf:

1
2
3
4
5
6
7
[client]
default-character-set = utf8

[mysqld]
default-storage-engine = INNODB
character-set-server = utf8
collation-server = utf8_general_ci

重启MySQL后生效。

** 如果MySQL的版本≥5.5.3,可以把编码设置为utf8mb4,utf8mb4和utf8完全兼容,但它支持最新的Unicode标准,可以显示emoji字符。 **

由于MySQL服务器以独立的进程运行,并通过网络对外服务,所以,需要支持Python的MySQL驱动来连接到MySQL服务器。
MySQL官方提供了mysql-connector-python驱动,但是安装的时候需要给pip命令加上参数--allow-external

1
$ pip install mysql-connector-python --allow-external mysql-connector-python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 导入MySQL驱动:
>>> import mysql.connector
# 注意把password设为你的root口令:
>>> conn = mysql.connector.connect(user='root', password='password', database='test')
>>> cursor = conn.cursor()
# 创建user表:
>>> cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')
# 插入一行记录,注意MySQL的占位符是%s:
>>> cursor.execute('insert into user (id, name) values (%s, %s)', ['1', 'Michael'])
>>> cursor.rowcount
1
# 提交事务:
>>> conn.commit()
>>> cursor.close()
# 运行查询:
>>> cursor = conn.cursor()
>>> cursor.execute('select * from user where id = %s', ('1',))
>>> values = cursor.fetchall()
>>> values
[('1', 'Michael')]
# 关闭Cursor和Connection:
>>> cursor.close()
True
>>> conn.close()

SQLAlchemy

数据库表是一个二维表,包含多行多列。
把一个表的内容用Python的数据结构表示出来的话,可以用一个list表示多行,list的每一个元素是tuple,表示一行记录。

1
2
3
4
5
6
# 包含id和name的user表:
[
('1', 'Michael'),
('2', 'Bob'),
('3', 'Adam')
]

Python的DB-API返回的数据结构就是像上面这样表示的。

1
2
3
4
5
6
7
8
9
10
11
# 如果把一个tuple用class实例来表示,就可以更容易地看出表的结构来:
class User(object):
def __init__(self, id, name):
self.id = id
self.name = name

[
User('1', 'Michael'),
User('2', 'Bob'),
User('3', 'Adam')
]

这就是ORM:Object-Relational Mapping,把关系数据库的表结构映射到对象上。

** 在Python中,最有名的ORM框架是SQLAlchemy。 **

1
$ pip install sqlalchemy
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
37
38
39
40
41
42
43
44
from sqlalchemy import Column, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

# 创建对象的基类:
Base = declarative_base()

# 定义User对象:
class User(Base):
# 表的名字:
__tablename__ = 'user'

# 表的结构:
id = Column(String(20), primary_key=True)
name = Column(String(20))

# 初始化数据库连接:
# '数据库类型+数据库驱动名称://用户名:口令@机器地址:端口号/数据库名'
engine = create_engine('mysql+mysqlconnector://root:password@localhost:3306/test')
# 创建DBSession类型:
DBSession = sessionmaker(bind=engine)

# --- 插入
# 创建session对象:
session = DBSession()
# 创建新User对象:
new_user = User(id='5', name='Bob')
# 添加到session:
session.add(new_user)
# 提交即保存到数据库:
session.commit()
# 关闭session:
session.close()

# --- 查询
# 创建Session:
session = DBSession()
# 创建Query查询,filter是where条件,最后调用one()返回唯一行,如果调用all()则返回所有行:
user = session.query(User).filter(User.id=='5').one()
# 打印类型和对象的name属性:
print('type:', type(user))
print('name:', user.name)
# 关闭Session:
session.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User(Base):
__tablename__ = 'user'

id = Column(String(20), primary_key=True)
name = Column(String(20))
# 一对多:
books = relationship('Book')

class Book(Base):
__tablename__ = 'book'

id = Column(String(20), primary_key=True)
name = Column(String(20))
# “多”的一方的book表是通过外键关联到user表的:
user_id = Column(String(20), ForeignKey('user.id'))

Web开发

随着PC的兴起,软件开始主要运行在桌面上,而数据库这样的软件运行在服务器端,这种Client/Server模式简称CS架构。
随着互联网的兴起,发现CS架构不适合Web,最大的原因是Web应用程序的修改和升级非常迅速,而CS架构需要每个客户端逐个升级桌面App,因此,Browser/Server模式开始流行,简称BS架构。

在BS架构下,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取Web页面,并把Web页面展示给用户即可。

Web开发经历了好几个阶段:
1.静态Web页面:由文本编辑器直接编辑并生成静态的HTML页面,如果要修改Web页面的内容,就需要再次编辑HTML源文件,早期的互联网Web页面就是静态的;
2.CGI:由于静态Web页面无法与用户交互,比如用户填写了一个注册表单,静态Web页面就无法处理。要处理用户发送的动态数据,出现了Common Gateway Interface,简称CGI,用C/C++编写。
3.ASP/JSP/PHP:由于Web应用特点是修改频繁,用C/C++这样的低级语言非常不适合Web开发,而脚本语言由于开发效率高,与HTML结合紧密,因此,迅速取代了CGI模式。ASP是微软推出的用VBScript脚本编程的Web开发技术,而JSP用Java来编写脚本,PHP本身则是开源的脚本语言。
4.MVC:为了解决直接用脚本语言嵌入HTML导致的可维护性差的问题,Web应用也引入了Model-View-Controller的模式,来简化Web开发。ASP发展为ASP.Net,JSP和PHP也有一大堆MVC框架。

HTTP

HTML是一种用来定义网页的文本,用来编写网页;
HTTP是在网络上传输HTML的协议,用于浏览器和服务器的通信。

HTTP请求的流程:
00.浏览器首先向服务器发送HTTP请求,请求包括:
01.方法:GET还是POST,GET仅请求资源,POST会附带用户数据(Body)
02.路径:/full/url/path
03.域名:由Host头指定:Host: www.sina.com.cn
04.以及其他相关的Header
10.服务器向浏览器返回HTTP响应,响应包括:
11.响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误
12.响应类型:由Content-Type指定
13.以及其他相关的Header
14.通常服务器的HTTP响应会携带内容,也就是有一个Body,包含响应的内容,网页的HTML源码就在Body中
20.如果浏览器还需要继续向服务器请求其他资源,比如图片,就再次发出HTTP请求,重复步骤00、10。

Web采用的HTTP协议采用了非常简单的请求-响应模式,从而大大简化了开发。
当编写一个页面时,只需要在HTTP请求中把HTML发送出去,不需要考虑如何附带图片、视频等。
浏览器如果需要请求图片和视频,它会发送另一个HTTP请求,因此,一个HTTP请求只处理一个资源。

HTTP协议同时具备极强的扩展性,虽然浏览器请求的是Sina的首页,但是Sina在HTML中可以链入其他服务器的资源,
比如,<img src="http://xx.cn/home/2.png">,从而将请求压力分散到各个服务器上,
而且,一个站点可以链接到其他站点,无数个站点互相链接起来,就形成了World Wide Web,简称WWW。

** 一个HTTP包含Header和Body两部分,其中Body是可选的。 **

1
2
3
4
5
6
# HTTP GET
GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
# 每个Header一行一个,换行符是\r\n
1
2
3
4
5
6
7
8
# HTTP POST
POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3

body data goes here...
# 当遇到连续两个\r\n时,Header部分结束,后面的数据全部是Body
1
2
3
4
5
6
7
8
# HTTP 响应
200 OK
Header1: Value1
Header2: Value2
Header3: Value3

body data goes here...
# HTTP响应如果包含body,也是通过\r\n\r\n来分隔的

Body的数据类型由Content-Type头来确定:
1.如果是网页,Body就是文本
2.如果是图片,Body就是图片的二进制数据。

当存在Content-Encoding时,Body数据是被压缩的,最常见的压缩方式是gzip,
当看到Content-Encoding: gzip时,需要将Body数据先解压缩,才能得到真正的数据。
压缩的目的在于减少Body的大小,加快网络传输。

HTTP权威指南

HTML

1
2
3
4
5
6
7
8
9
<!-- HTML -->
<html>
<head>
<title>Hello</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
<html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- CSS -->
<html>
<head>
<title>Hello</title>
<style>
h1 {
color: #333333;
font-size: 48px;
text-shadow: 3px 3px 3px #666666;
}
</style>
</head>
<body>
<h1>Hello, world!</h1>
</body>
<html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- JavaScript -->
<html>
<head>
<title>Hello</title>
<style>
h1 {
color: #333333;
font-size: 48px;
text-shadow: 3px 3px 3px #666666;
}
</style>
<script>
function change() {
document.getElementsByTagName('h1')[0].style.color = '#ff0000';
}
</script>
</head>
<body>
<h1 onclick="change()">Hello, world!</h1>
</body>
<html>

WSGI接口

Web应用的本质就是:
1.浏览器发送一个HTTP请求
2.服务器收到请求,生成一个HTML文档
3.服务器把HTML文档作为HTTP响应的Body发送给浏览器
4.浏览器收到HTTP响应,从HTTP的Body中取出HTML文档并显示

简单的Web应用就是先把HTML用文件保存好(静态):
使用一个现成的HTTP服务器软件,接收用户请求,从文件中读取HTML,返回。
常见的静态服务器软件:Apache、Nginx、Lighttpd等。

如果要动态生成HTML,就需要通过一个专门的服务器软件来实现:“接受HTTP请求”、“解析HTTP请求”、“发送HTTP响应”等苦力活,而使用Python专注于生成HTML文档就可以了。
Python和专门的服务器软件之间交互使用的统一的接口就是WSGI:Web Server Gateway Interface。

1
2
3
4
5
# hello.py
# 简易Web版Hello-World
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return [b'<h1>Hello World</h1>']

上面的application函数就是一个符合WSGI标准的HTTP处理函数:
1.参数environ是一个包含所有HTTP请求信息的dict
2.参数start_response是一个发送HTTP响应的函数(只能调用一次)
3.返回值将作为HTTP响应的Body发送给浏览器
4.函数application必须由WSGI服务器调用

有了WSGI,编写代码时只需要关心:
1.从environ这个dict中取出HTTP请求信息
2.构造HTML
3.通过start_response发送Header
4.返回Body

Python内置一个WSGI服务器wsgiref,它是用纯Python编写的WSGI服务器“参考实现”。
** 所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何效率,仅供开发和测试使用。 **

1
2
3
4
5
6
7
8
9
10
11
12
# server.py
from wsgiref.simple_server import make_server

import hello

# 创建一个服务器
# IP地址为空
# 监听端口为8000
# 处理函数是hello.application
httpd = make_server('', 8000, hello.application)
# 开始监听HTTP请求
httpd.serve_forever()
1
2
3
4
5
6
# hello.py
# 会根据浏览器地址栏的输入来改变HTML内容
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'textml')])
body = '<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web')
return [body.encode('utf-8')]

Web框架

常见的Python Web框架有:
1.Flask:小巧、灵活
2.Django:全能型Web框架
3.web.py:一个小巧的Web框架
4.Bottle:和Flask类似的Web框架
5.Tornado:Facebook的开源异步Web框架

有了Web框架,在编写Web应用时,注意力就从“WSGI处理函数”转移到“URL+对应的处理函数”,这样,编写Web应用就更加简单了。

在编写URL处理函数时,除了配置URL外,从HTTP请求拿到用户数据也是非常重要的。
Web框架都提供了自己的API来实现这些功能。
Flask通过request.form['name']来获取表单的内容。

1
$ pip install flask
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
from flask import Flask
from flask import request

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def home():
return '<h1>Home</h1>'

@app.route('/signin', methods=['GET'])
def signin_form():
return '''<form action="/signin" method="post">
<p><input name="username"></p>
<p><input name="password" type="password"></p>
<p><button type="submit">Sign In<tton></p>
</form>'''

@app.route('/signin', methods=['POST'])
def signin():
# 需要从request对象读取表单内容:
if request.form['username']=='admin' and request.form['password']=='password':
return '<h3>Hello, admin!</h3>'
return '<h3>Bad username or password.</h3>'

if __name__ == '__main__':
app.run()

模板

MVC:Model-View-Controller 模型-视图-控制器

Python处理URL的函数就是C:Controller,Controller负责业务逻辑,比如检查用户名是否存在,取出用户信息等;
包含变量{{ name }}的模板就是V:View,View负责显示逻辑,通过简单地替换一些变量,View最终输出就是用户看到的HTML;
Model是用来传给View的,View在替换变量的时候,是从Model中取出相应的数据,上例中,Model就是一个dict{'name': 'Michael'},由于Python支持关键字参数,很多Web框架允许传入关键字参数,然后在框架内部组装出一个dict作为Model。

Flask通过render_template函数来实现模板的渲染。
和Web框架类似,Python的模板也有很多种。
Flask默认支持的模板是jinja2

1
$ pip install jinja2
1
2
3
4
5
6
7
8
9
$ tree .
.
├── app.py
└── templates
├── form.html
├── home.html
└── signin-ok.html

1 directory, 4 files
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# app.py
from flask import Flask, request, render_template

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def home():
return render_template('home.html')

@app.route('/signin', methods=['GET'])
def signin_form():
return render_template('form.html')

@app.route('/signin', methods=['POST'])
def signin():
username = request.form['username']
password = request.form['password']
if username=='admin' and password=='password':
return render_template('signin-ok.html', username=username)
return render_template('form.html', message='Bad username or password', username=username)

if __name__ == '__main__':
app.run()
1
2
3
4
5
6
7
8
9
<!-- home.html -->
<html>
<head>
<title>Home</title>
</head>
<body>
<h1 style="font-style:italic">Home</h1>
</body>
<html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- form.html -->
<html>
<head>
<title>Please Sign In</title>
</head>
<body>
{% if message %}
<p style="color:red">{{ message }}</p>
{% endif %}
<form action="/signin" method="post">
<legend>Please sign in:</legend>
<p><input name="username" placeholder="Username" value="{{ username }}"></p>
<p><input name="password" placeholder="Password" type="password"></p>
<p><button type="submit">Sign In<tton></p>
</form>
</body>
<html>
1
2
3
4
5
6
7
8
9
<!-- signin-ok.html -->
<html>
<head>
<title>Welcome, {{ username }}</title>
</head>
<body>
<p>Welcome, {{ username }}!</p>
</body>
<html>

只需要在Python代码中处理M:Model和C:Controller,而V:View是通过模板处理的,这样就把Python代码和HTML代码最大限度地分离了。
使用模板的另外一个好处是,模板改起来很方便,而且,改完保存后,刷新浏览器就能看到最新的效果。

在Jinja2中,如果需要循环、条件判断等指令语句,可以用{% ... %}表示指令。

1
2
3
4
5
<!-- 循环输出页码 -->
{% for i in page_list %}
<a href="/page/{{ i }}">{{ i }}</a>
{% endfor %}
<!-- 如果page_list是list:[1, 2, 3, 4, 5],上面的模板将输出5个超链接。 -->

异步IO

CPU的速度远远快于磁盘、网络等IO。
在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。
这种情况称为同步IO

在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。

因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以必须使用多线程或者多进程来并发执行代码,为多个用户服务。
每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。

多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。
由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。

由于要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的方法之一。

另一种解决IO问题的方法是异步IO
当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。
一段时间后,当IO返回结果时,再通知CPU进行处理。

可以想象如果按普通顺序写出的代码实际上是没法完成异步IO的:

1
2
3
4
5
do_some_code()
f = open('/path/to/file', 'r')
r = f.read() # <== 线程停在此处等待IO操作结果
# IO操作完成后线程才能继续执行:
do_some_code(r)

异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:

1
2
3
4
loop = get_event_loop()
while True:
event = loop.get_event()
process_event(event)

消息模型其实早在应用在桌面应用程序中了。
一个GUI程序的主线程就负责不停地读取消息并处理消息。
所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后由GUI程序的主线程处理。
由于GUI线程处理键盘、鼠标等消息的速度非常快,所以用户感觉不到延迟。
某些时候,GUI线程在一个消息处理的过程中遇到问题导致一次消息处理时间过长,此时,用户会感觉到整个GUI程序停止响应了,敲键盘、点鼠标都没有反应。
这种情况说明在消息模型中,处理一个消息必须非常迅速,否则,主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。

当遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。
当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。

在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。
这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。
对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

协程

协程,又称微线程,纤程。英文名Coroutine
协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
** 子程序调用是通过栈实现的,一个线程就是执行一个子程序。 **
** 子程序调用总是一个入口,一次返回,调用顺序是明确的。 **

协程的调用和子程序不同。

协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。

比如子程序A、B:

1
2
3
4
5
6
7
8
9
def A():
print('1')
print('2')
print('3')

def B():
print('x')
print('y')
print('z')

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:

1
2
3
4
5
6
1
2
x
y
3
z

看起来A、B的执行有点像多线程。
** 协程的特点在于是一个线程执行。 **

和多线程比,协程有优势:
1.协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
2.不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

** “多进程+协程”:既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。 **

Python对协程的支持是通过generator实现的。

generator中,不仅可以通过for循环来迭代,还可以不断调用next函数获取由yield语句返回的下一个值。

但是Python的yield不仅可以返回一个值,还可以接收调用者发出的参数。

来看例子:
传统的“生产者-消费者模型”是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'

def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()

c = consumer()
produce(c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator,把一个consumer传入produce后:
1.调用c.send(None)启动生成器
2.通过c.send(n)切换到consumer执行
3.consumer通过yield拿到消息,处理,又通过yield把结果传回
4.produce拿到consumer处理的结果,继续生产下一条消息
5.produce决定不生产了,通过c.close()关闭consumer,整个过程结束

整个流程无锁,由一个线程执行,produceconsumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

最后套用Donald Knuth的一句话总结协程的特点:
** “子程序就是协程的一种特例。” **

asyncio

asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。

asyncio的编程模型就是一个消息循环。
asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio

@asyncio.coroutine
def hello():
print("Hello World!")
# 异步调用asyncio.sleep(1):
r = yield from asyncio.sleep(1)
print("Hello Again!")

# 获取EventLoop:
loop = asyncio.get_event_loop()
# 执行coroutine
loop.run_until_complete(hello())
loop.close()

装饰器@asyncio.coroutine把一个generator标记为coroutine类型,然后,就可以把这个coroutine扔到EventLoop中执行。

hello()会首先打印出Hello World!,然后,yield from语法可以调用另一个generator
由于asyncio.sleep()也是一个coroutine,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。
asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。

asyncio.sleep(1)看成是一个耗时1秒的IO操作。
在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了。
因此可以用“一个线程实现并发执行”。

1
2
3
4
5
6
7
8
9
10
11
12
13
import threading
import asyncio

@asyncio.coroutine
def hello():
print('Hello World! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
print('Hello Again! (%s)' % threading.currentThread())

loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
1
2
3
4
5
Hello World! (<_MainThread(MainThread, started 140426655303488)>)
Hello World! (<_MainThread(MainThread, started 140426655303488)>)
(暂停约1秒)
Hello Again! (<_MainThread(MainThread, started 140426655303488)>)
Hello Again! (<_MainThread(MainThread, started 140426655303488)>)

根据打印的当前线程名称可以看出,两个coroutine是由同一个线程并发执行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import asyncio

@asyncio.coroutine
def wget(host):
print('wget %s...' % host)
connect = asyncio.open_connection(host, 80)
reader, writer = yield from connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
yield from writer.drain()
while True:
line = yield from reader.readline()
if line == b'\r\n':
break
print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
# Ignore the body, close the socket
writer.close()

loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
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
37
38
wget www.sina.com.cn...
wget www.163.com...
wget www.sohu.com...
www.sohu.com header > HTTP/1.1 200 OK
www.sohu.com header > Content-Type: text/html;charset=UTF-8
www.sohu.com header > Connection: close
www.sohu.com header > Server: nginx
www.sohu.com header > Date: Wed, 19 Jul 2017 10:04:08 GMT
www.sohu.com header > Cache-Control: max-age=60
www.sohu.com header > X-From-Sohu: X-SRC-Cached
www.sohu.com header > Content-Encoding: gzip
www.sohu.com header > FSS-Cache: HIT from 13670775.18717057.21608900
www.sohu.com header > FSS-Proxy: Powered by 10066240.11508042.18004310
www.sina.com.cn header > HTTP/1.1 200 OK
www.sina.com.cn header > Server: nginx
www.sina.com.cn header > Date: Wed, 19 Jul 2017 10:04:36 GMT
www.sina.com.cn header > Content-Type: text/html
www.sina.com.cn header > Content-Length: 598104
www.sina.com.cn header > Connection: close
www.sina.com.cn header > Last-Modified: Wed, 19 Jul 2017 10:01:42 GMT
www.sina.com.cn header > Vary: Accept-Encoding
www.sina.com.cn header > Expires: Wed, 19 Jul 2017 10:05:35 GMT
www.sina.com.cn header > Cache-Control: max-age=60
www.sina.com.cn header > X-Powered-By: shci_v1.03
www.sina.com.cn header > Age: 16
www.sina.com.cn header > Via: http/1.1 ctc.tianjin.ha2ts4.39 (ApacheTrafficServer/4.2.1.1 [cRs f ]), http/1.1 ctc.nanjing.ha2ts4.93 (ApacheTrafficServer/4.2.1.1 [cHs f ])
www.sina.com.cn header > X-Cache: HIT.39
www.sina.com.cn header > X-Via-CDN: f=edge,s=ctc.nanjing.ha2ts4.92.nb.sinaedge.com,c=101.81.26.186;f=Edge,s=ctc.nanjing.ha2ts4.93,c=202.102.94.92;f=edge,s=ctc.tianjin.ha2ts4.40.nb.sinaedge.com,c=202.102.94.93;f=Edge,s=ctc.tianjin.ha2ts4.39,c=202.102.94.59
www.sina.com.cn header > X-Cache: HIT.93
www.163.com header > HTTP/1.1 200 OK
www.163.com header > Expires: Wed, 19 Jul 2017 10:05:56 GMT
www.163.com header > Date: Wed, 19 Jul 2017 10:04:36 GMT
www.163.com header > Server: nginx
www.163.com header > Content-Type: text/html; charset=GBK
www.163.com header > Vary: Accept-Encoding,User-Agent,Accept
www.163.com header > Cache-Control: max-age=80
www.163.com header > X-Via: 1.1 qzh157:1 (Cdn Cache Server V2.0), 1.1 adxxz40:5 (Cdn Cache Server V2.0)
www.163.com header > Connection: close

async/await

Python从3.5版本开始为asyncio提供了asyncawait的新语法。

asyncawait是针对coroutine的新语法,可以让coroutine的代码更简洁易读。
要使用新的语法,只需要做两步简单的替换:
1.把@asyncio.coroutine替换为async
2.把yield from替换为await

1
2
3
4
5
6
7
import asyncio

@asyncio.coroutine
def hello():
print("Hello World!")
r = yield from asyncio.sleep(1)
print("Hello Again!")

用新语法重新编写如下:

1
2
3
4
5
6
import asyncio

async def hello():
print("Hello World!")
r = await asyncio.sleep(1)
print("Hello Again!")

aiohttp

asyncio可以实现单线程并发IO操作。
如果仅用在客户端,发挥的威力不大。
如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用“单线程+coroutine”实现多用户的高并发支持。
asyncio实现了TCP、UDP、SSL等协议。

aiohttp则是基于asyncio实现的HTTP框架。

1
$ pip install aiohttp
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
import asyncio

from aiohttp import web

async def index(request):
await asyncio.sleep(0.5)
return web.Response(body=b'<h1>Index</h1>')

async def hello(request):
await asyncio.sleep(0.5)
text = '<h1>hello, %s!</h1>' % request.match_info['name']
return web.Response(body=text.encode('utf-8'))

# aiohttp的初始化函数init也是一个coroutine
async def init(loop):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', index)
app.router.add_route('GET', '/hello/{name}', hello)
# loop.create_server利用asyncio创建TCP服务
srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
print('Server started at http://127.0.0.1:8000...')
return srv

loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()