“廖雪峰的官方网站 - Python教程”的阅读笔记。

Python简介

  • C语言是可以用来编写操作系统的贴近硬件的语言,适合开发那些追求运行速度、充分发挥硬件性能的程序。

  • Python是用来编写应用程序的高级编程语言,有大量基础库和第三方库可直接使用。适合:

    • 网络应用:如网站、后台服务。
    • 日常小工具:如脚本任务。
    • 把其它语言开发的程序包装起来。
  • Python缺点:

    • 速度慢。
    • 代码不加密。

安装Python

  • 2.x和3.x不兼容,教程以3.5为准。

  • 使用homebrew安装:brew install python3

  • 常见Python解释器

    • CPython:官方默认。(提示符:>>>)
    • IPythion:基于CPython,在交互上有所增强。(提示符:In[序号])
    • PyPy:利用动态编译提升速度,可能和CPython的执行结果不同。
    • Jython:运行在Java平台,编译成Java字节码执行。
    • IronPython:运行在.Net平台。

第一个Python程序

  • 在Python交互式环境下,输入表达式可以直接看到结果。
  • 如果表达式写在abc.py文件中,必须写在print()里,才能通过python abc.py打印出来。

  • 如果想能够直接在shell中运行py文件,需要在py文件第一行加上以下内容:#!/usr/bin/env python3(Windows会忽略这一行),同时赋予执行权限:chmod a+x hello.py

输出

  • print('hello, world')
  • print('hello', 'world') 遇到逗号会输出一个空格。

输入

  • name = input()
  • name = input('please input your name: ')

Python基础

  • #这是注释
  • 每一行是一个语句,若语句以:结尾,后面缩进的语句为代码块。最好以4个空格为缩进。
  • 大小写敏感。
  • Python是动态语言。

数据类型

  • 整数:1,100,0xff00
  • 浮点数:1.23,1.2e-9
  • 字符串:’abc’,”xyz”,”I’m OK”,”I\’m \”OK\”\n”

    • 不转义:r’\\\t\n\\’
    • 多行字符串:

      1
      2
      3
      r'''line1
      line2
      line3'''
  • 布尔值:True,False(运算:and,or,not)

  • 空值:None

类型转换

  • int()float()str()bool()可以实现数据类型转换。

变量

  • 执行 a = 'ABC'时:
    • 在内存中创建了字符串’ABC’。
    • 在内存中创建了变量a,让它指向’ABC’。

除法

  • 10 / 3得3.333333。
  • 10 // 3得3。
  • 10 % 3得1。

字符串和编码

编码常识

  • ASCII编码是1个字节,Unicode编码通常是2个字节。
  • UTF-8编码是可变长编码,可以是1~6个字节。英文字母为1个字节,汉字通常3个字节。
  • 内存中统一用Unicode编码,存储和传输时使用UTF-8编码。

字符串和编码转换

  • Python3中,字符串是以Unicode编码的,支持多语言。使用chr()ord()可以对字符进行编码转换:
1
2
3
4
5
6
7
8
>>> ord('A')
65
>>> ord('中')
20013
>>> chr(66)
'B'
>>> chr(25991)
'文'
  • 在内存中,字符串类型以Unicode表示。但如果要保存或传输,需要转变为以字节为单位的bytes。使用b'ABC'来表示。使用encode()方法可以将字符串转换为指定编码的bytes:
1
2
3
4
5
6
>>> 'ABC'.encode('ascii')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe64\xb8\xad\xe6\x96\x87'
>>> '中文'.encode('ascii')
(UnicodeEncodeError....)
  • 反之,可以使用decode()方法:
1
2
3
4
>>> b'ABC'.decode('ascii')
'ABC'
>>> b'\xe64\xb8\xad\xe6\x96\x87'
'中文'
  • 计算字符串的字符数,使用len('ABC中文'),得到结果5
  • 计算bytes的字节数,使用len(b'ABC\xe4'),得到结果4

源代码的编码

  • Python源代码文件包含中文时要使用UTF-8编码,需要在文件开头加上:
1
2
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

同时要注意文本编辑器使用UTF-8 without BOM编码。

字符串的格式化

1
2
3
4
5
6
>>> 'Hi, %s, you have $%d. ' % ('Michael', 10000)
'Hi, Michael, you have $10000.
>>> '%2d----%02d' % (3, 1)
' 3----01'
>>> '%.2f' % 3.14159
'3.14'
  • %d:整数
  • %f:浮点数
  • %x:十六进制整数
  • %s:字符串(从任意类型转换)

注意:%的转义使用%%

字符串的不可变性

str.replace('a','A')会返回替换的结果,但不会改变str。

使用list和tuple

  • list:可变,有序表,数据类型可不同,可嵌套。
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> classmates = ['A', 'B', 'C', 1, 2, 3, [11, 22], True]
>>> len(classmates)
7
>>> classmates[0]
'A'
>>> classmates[-1]
True
>>> classmates.append('D')
>>> classmates.insert(1, 'AB')
>>> classmates.pop()
'D'
>>> classmates.pop(0)
'A'
  • tuple:元组,只读的有序表。注意若有元素是list,该list的内容是可变的。
1
2
3
>>> t = (1, 2)
>>> t = (1,)
>>> t = ('a','b', [1, 2])

条件判断

  • 注意这里使用了int()将input的字符串转化成整型。
1
2
3
4
5
6
7
8
age = input('input your age: ')
age = int(age)
if age >= 6:
print('teenager');
elif age >= 18:
print('adult');
else
print('kid');

循环

  • 注意这里使用了range(101)来生成[0,1,......,100]
1
2
3
4
5
6
7
8
9
10
11
12
13
names = ['Michael', 'Bob', 'Tracy']
for name in names:
print(name)

sum = 0
for x in range(101):
sum = sum + x
print(sum)

n = 10
while n > 0:
n = n - 1
print(n)

使用dict和set

  • dict:字典,无序的,常数时间查找
1
2
3
4
5
6
7
8
9
10
11
>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Adam'] = 67
>>> d['Bob']
75
>>> 'Thomas' in d
False
>>> d.get('Thomas') #返回None时Python不显示结果
>>> d.get('Thomas', -1) #自己指定找不到key时返回的value
-1
>>> d.pop('Bob')
75
  • set:一组key的集合,没有重复。和dict一样,都不能把可变的元素放入,如list。
1
2
3
4
5
6
7
8
9
10
11
12
>>> s = set([1, 1, 2, 2, 3])
>>> s
{1, 2, 3}
>>> s.add(4)
>>> s.add(4)
>>> s.remove(4)

>>> t = set([2, 3, 4])
>>> s & t
{2, 3}
>>> s | t
{1, 2, 3, 4}

函数

  • 调用函数时,参数数量不对或者参数类型不对都会导致TypeError错误。
  • max()函数可以支持任意多个参数。
  • 函数名是一个函数对象的引用,可以将其复制给一个变量,相当于给函数起了一个别名。

定义函数

1
2
3
4
5
def my_abs(s):
if x >= 0:
return x
else:
return -x
  • 如果没有返回值,函数执行完毕会返回None。
  • 在交互环境中,定义函数时Python会出现...的提示,定义结束后需要按两次回车。

导入函数

  • 假设my_abs()定义在abstest.py中,在该目录的交互式环境中,可以使用from abstest import my_abs来导入该函数。

空函数

  • 用pass表示占位符,什么都不做:
1
2
def nop():
pass

检查参数类型

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

返回多个值

  • 利用tuple(省略了小括号):
1
2
3
def move(x, y, step, angle):
...
return x*x, y*y

函数参数

默认参数:同C语言,如果只是指定个别的默认参数,可以这样写:

enroll('Adam', city = 'Tianjin')

注意:默认参数一定要指向不变对象,比如如果是个List,每次调用改变这个List值,这个状态会持续到下次调用。比如:

1
2
3
def add_end(L = []):
L.append('End')
return L

这里最好使用None:

1
2
3
4
5
def add_end(L = None):
if L is None:
L = []
L.append('End')
return L

可变参数

  • 允许传入0个或任意个参数,组装为一个tuple。
1
2
3
4
5
def calc(*numbers):
sum = 0
for n in numbers
sum = sum + n * n
return sum

说明:

  • 如果去掉*,则需要传入一个list或者tuple。
  • 传入0个参数也是可以的。
  • 也可以使用list或者tuple前面加*的方式进行调用:
1
2
>>> num = [1, 2, 3]
>>> calc(*nums)

关键字参数

  • 允许传入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'}

注意:函数里对kw的改动不会影响到实参。

命名关键字参数

  • 对关键字参数的key进行限制,如:只接受city和job:
1
2
def person(name, age, *, city, job):
print(name, age, city, job)
  • 如果已经有了一个可变参数,后面的命名关键字参数就不需要用*分割了,但这时调用必须传入参数名,可以有缺省值:
1
2
def person(name, age, *args, city = 'Beijing', job):
print(name, age, args, city, job)

参数组合

  • 参数定义顺序:必选参数、默认参数POSITIONAL_OR_KEYWORD、可变参数VAR_POSITIONAL、命名关键字参数KEYWORD_ONLY、关键字参数VAR_KEYWORD
  • 其实还有一种POSITIONAL_ONLY的参数,只有一些内置函数用到。
  • 使用inspect.signature(func).parameters[i].kind获取函数的参数信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
def f1(a, b, c = 0, *args, **kw):
pass
def f2(a, b, c = 0, *, d, **kw):
pass

>>> f1(1, 2)
>>> f1(1, 2, c = 3)
>>> f1(1, 2, 3, 'a', 'b')
>>> f1(1, 2, 3, 'a', 'b', x = 99)
>>> f2(1, 2, d = 99, ext = None)
>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw) #对任何函数,都可以这样进行调用!

递归函数

  • 小心栈溢出!对于没有循环语句的语言,可以使用尾递归优化策略,让函数返回的时候,调用自身而不包含表达式。但是大多数语言没有针对尾递归做优化,包括Python解释器。

高级特性

切片

  • 取List的部分元素:
1
2
3
4
5
6
7
8
>>> L[0:3]                  #从下标0开始取3个元素
>>> L[:3] #0可以省略
>>> L[-3:-1]
>>> L[-3:]
>>> L[:10:2] #前10个数,每两个取一个
>>> L[:] #复制一个list
>>> (0, 1, 2, 3, 4)[:3] #tuple的切片仍为tuple
>>> 'ABCDEFG'[:3] #字符串也可以切

迭代

  • 即for…in…,可迭代对象有:list、tuple、dict、str。
  • 注意dict的三种迭代方式:

    • for key in d
    • for value in d.values()
    • for k, v in d.items()
  • 注意list的下标迭代方式:

    • for i, value in enumerate(['A', 'B', 'C']):
  • 判断一个对象是否可迭代:

1
2
3
>>> from collections import Iterable
>>> isinstance(123, Iterable)
False

列表生成式(List Comprehensions)

1
2
3
4
5
6
7
8
9
10
11
12
>>> list(range(1,11))                               #生成列表[1, 2, 3, ..., 10]
>>> [x * x for x in range(1,11)] #生成列表[1, 4, 9, ..., 100]
>>> [x * x for x in range(1,11) if x % 2 == 0] #生成列表[4, 16, 36, ..., 100]
>>> [m + n for m in 'ABC' for n in 'XYZ'] #生成列表['AX', 'AY', ..., CZ'](全排列)
>>> d = {'x': 'A', 'y': 'B', 'z': 'C'}
>>> [k + '=' + v for k, v in d.items()] #生成列表['y=B', 'x=A', 'z=c']

>>> import os
>>> [d for d in os.listdir('.')] #生成当前目录下的文件和目录构成的列表

>>> L = ['Hello', 'World']
>>> [s.lower() for s in L] #把L总的字符串都变成小写

生成器(generator)

  • 把列表生成式的[]改成()即可。
  • 边循环边计算,节约空间。
  • 通过next(g)函数获取下一个返回值。或者使用for…in。
  • 也可以通过函数生成,利用yield:
1
2
3
4
5
6
7
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'

注意,generator函数每次调用next()时执行,遇到yield语句返回,再次执行时从上次返回的yield语句出继续执行。

  • 通过调用该函数将得到一个可迭代的generator对象。

  • 如果想捕获该函数的返回值,需要捕获StopIteration错误:

    1
    2
    3
    4
    5
    6
    7
    while True:
    try:
    x = next(g)
    print('g:', x)
    except StopIteration as e:
    print('Generator return Value:', e.value)
    break;

迭代器(Iterator)

  • 可用于for循环的对象统称为可迭代对象:Iterable。
  • 可以使用isinstance(x, Iterable)来判断是否是可迭代对象。
  • 生成器还可以使用next()不断调用并返回下一个值,直至StopIteration错误。这种对象称为迭代器。
  • 可以使用isinstance(x, Iterator)来判断是否是迭代器。
  • 可以使用iter(x)函数把list、dict、str等Iterable变成Iterator。
  • for … in 的等价写法:
1
2
3
4
5
6
7
8
9
for x in [1, 2, 3 ,4]:
pass

it = iter([1, 2, 3, 4])
while True:
try:
x = next(it)
except StopIteration:
break;

函数式编程

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

高阶函数

  • 变量可以指向函数。
  • 函数名也是变量。(不是只读的)
  • 函数名作参数。

map/reduce

  • map()接受两个参数:函数和Iterable,map将函数依次作用在每个元素上,把结果作为新的Iterator返回。
1
2
3
4
5
6
def f(x):
return x * x

>>> r = map(f, [1, 2, 3 ,4])
>>> list(r)
[1, 4, 9, 16]
  • reduce()也接受这样的两个参数,函数把上一次计算结果与下一个元素做累积计算。即:`reduce(f, [x1, x2, x3,x4]) = f(f(f(x1, x2), x3), x4)
1
2
3
4
5
6
>>> from functools import reduce
>>> def add(x, y):
... return x + y
...
>>> reduce(add, [1, 3, 5, 7])
16
  • 利用这两个函数,可以实现int(str)的功能:
1
2
3
4
5
6
7
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));

还可以使用lambda函数进一步简化:

1
2
3
4
5
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):
return reduce(lambda x, y: x * 10 + y, map(char2num, s))

filter

  • 类似map,不过函数返回True/False,决定元素是否保留。返回的也是Iterator。

  • 实例:利用埃氏算法计算素数:

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)

#测试:
for n in primes():
if n < 1000:
print(n)
else
break

sorted

1
2
3
4
5
6
7
8
>>> sorted([3, 2 , 4, 1])
[1, 2, 3, 4]
>>> sorted([-3, 2, 4, 1], key = abs)
[1, 2, -3, 4]
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']

返回函数

1
2
3
4
5
6
7
8
9
10
11
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, 4)
>>> f()
10
  • 内部函数sum可以访问外部函数lazy_sum的参数和局部变量。当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中。————“闭包(Closure)”
  • 每次调用lazy_sum返回的都是个新函数。

使用闭包的注意事项

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

f1, f2, f3 = count()

>>> f1()
>>> f2()
>>> f3()

注意:这里三次执行的结果都是9,因为返回的函数引用了变量i,但它并非立即执行。等到3个函数都返回时,所引用的变量i都变成了3.

所以:返回闭包时,返回函数不要引用任何循环变量等后续会发生变化的变量。

如果一定要引用循环变量,可以再引入一个函数(可以使用lambda函数缩短代码):

1
2
3
4
5
6
7
8
9
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i))
return fs

匿名函数

  • lambda x: x * x实际上就是def f(x): return x * x
  • Python对匿名函数支持有限,只有一些简单的情况下可以使用。

装饰器

  • 函数有个__name__属性,可以拿到函数的名字:
1
2
3
4
5
6
7
8
>>> def now():
... print('2016-3-3')
...
>>> f = now
>>> now.__name__
'now'
>>> f.__name__
'now'
  • 不修改now函数,实现在执行now之前打印日志:
1
2
3
4
5
6
7
8
9
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper

@log
def now():
print('2016-3-3')
  • 这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。实际上就是一个返回函数的高阶函数。
  • @log放在now()定义处,相当于执行了:now = log(now),实际上调用的是wrapper()函数,根据定义,它可以接受任意参数的调用。
  • 如果decorator本身需要传入参数,需要再编写一个返回decorator的高阶函数。比如要自定义log的文本:
1
2
3
4
5
6
7
8
9
10
11
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % {text, func.__name__})
return func(*args, **kw)
return wrapper
return decorator

@log('execute')
def now():
print('2016-3-3')
  • 相当于:now = log('execute')(now)
  • 注意:这时函数的__name__已经由'now'变成了'wrapper'。如果代码依赖函数签名,需要避免这一点,不需要编写wrapper.__name__ = func.__name__这样的代码,而可以直接这么写:
1
2
3
4
5
6
7
8
import functools

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

或者针对带参数的decorator:

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(...)
return func(*args, **kw)
return wrapper
return decorator

偏函数

  • 对某个函数的个别参数设定默认值,得到一个新的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> int('12345', base=8)
5349

#常规方法
def int8(x, base=8):
return int(x, base)

#使用偏函数的写法
import functools
int2 = functools.partial(int, base=2)

int2('10010')
#就相当于固定了一个关键字参数:
kw = {'base': 2}
int('10010', **kw)

#也可接受*args:
max2 = functools.partial(max, 10)
max2(5, 6, 7)
#就相当于:
args = (10, 5, 6, 7)
max(*args)

模块

  • 文件abc.py就是一个名字叫abc的模块(Module)。
  • 多个模块可以放在同一个模块下组成一个包(Package)。包名即目录名。
  • 包下一定要有一个__init__.py的文件。
  • 引入包后,模块的名字就变成了包名.模块名
  • 不要和自带的模块冲突,如sys模块。

使用模块

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

' a test module '

__author__ = 'Meng Fanze'

import sys

def test():
args = sys.argv
if len(args) == 1:
print('Hello world')
elif len(args) == 2:
print(' Hello %s' % args[1])
else:
print('Too many arguments')

if __name__ == '__main__':
test()
  • 第一个字符串:文档注释。
  • __author__:作者。
  • 导入了sys模块后,就有了变量sys指向该模块。它有一个argv变量,用list存储了所有参数,且至少有一个元素,即文件名。如:python3 hello.py获得的sys.argv就是[‘hello.py’]。
  • 最后一个if可以让模块从命令行运行时执行一段额外的代码,常用于测试。原理是Python解释器会把__name__置为__main__

作用域

  • 公开的:正常的函数和变量名。
  • 特殊的:__author____name____doc__(文档注释),可以被直接饮用,但有特殊用途。
  • 非公开的:_xxx__xxx不应该被直接引用。

第三方模块

  • 包管理工具:pip(Mac和Linux已自带)
  • 第三方库会在Python官方的pypi.python.org注册,如安装Pillow(基于Python Imaging Library,PIL的处理图像的工具库):
1
pip install Pillow
  • 使用import:
1
2
3
4
5
6
>>> from PIL import Image
>>> im = Image.open('test.jpg')
>>> print(im.format, im.size, im.mode)
PNG (400, 300) RGB
>>> im.thumbnail((200,100))
>>> im.save('thumb.jpg', 'JPEG')
  • Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块。搜索路径位于sys模块的sys.path变量中。

  • 临时添加自己的搜索目录:

1
2
>>> import sys
>>> sys.path.append('/....')
  • 永久添加自己的搜索路径:设置环境变量PYTHONPATH,在其中只放置自己添加的路径即可。

面向对象编程

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

def __init__(self, name, score):
self.name = name
self.score = score

def print_score(self):
print('%s: %s' % (self.name, self.score))

>>> bart = Student()
  • object表示基类,object是最基本的基类。
  • 可以自由地给实例绑定属性。
  • __init__相当于构造函数,第一个参数永远是self,表示实例本身。
  • 有了init之后,创建实例时就要按照对应的参数表传参了,但self不用传。
  • 实际上所有类中的方法都带有这样的self。

访问限制

  • private变量前加上两个下划线。
  • 注意,特殊变量(即__xxx__)不是私有变量。
  • 加一个下划线的约定俗成为“不推荐访问”的变量。
  • private变量其实是可以从外部访问的……(什么鬼= =!),比如,Python会把__name解析成_Student__name,通过它即可来实现访问。但是,不同版本的解释器可能不同。
  • Python本身没有任何机制阻止你干坏事,一切靠自觉。

继承和多态

  • 思想同C++。
  • “开闭原则”:对扩展开发,对修改封闭。
  • isinstance(dog, Animal)返回True。
  • 和静态语言不同的是,调用一个对象的方法,这个对象不一定必须要是基类或者其派生类,还可以是任何具有该方法的类。着一定决定了继承不像静态语言那样是必须的。

获取对象信息

  • type()可以返回对象类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> type(123) == type(456)
True
>>> type(123) == int
True

>>> 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
>>> isinstance([1, 2, 3], (list, tuple))
True
  • dir()可以获得一个对象的所有属性和方法。返回一个list。
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']
  • len('ABC')实际上就是调用对象的__len__()方法,所以自己写的类中实现该方法就也可以使用len()进行调用。

  • getattr(obj, 'attr1')可以获取属性。

  • setattr(obj, 'attr1', 'value')可以设置属性。
  • hasattr(obj, 'attr1')可以判断属性是否存在。
  • 如果试图获取不存在的属性,会报AttributeError。
  • getattr(obj, 'attr1', 'defaultValue')可以获取属性,若属性不存在,返回默认值。
  • 以上,属性也可以是方法。

实例属性和类属性

  • 直接为类定义属性视作类的静态成员。
1
2
class Student(object):
name = 'Student'
  • 可以用Student.name访问,也可以用其实例对象访问,如s.name,前提是s没有name成员。

面向对象高级编程

使用slots

  • 给实例绑定方法,对另一个实例是不起作用的:
1
2
3
4
5
def set_age(self, age):
self.age = age

from types import MethodType
s.set_age = MethodType(set_age, s)
  • 为了给所有实例绑定方法,可以给class绑定方法:
1
2
3
4
def set_score(self, score):
self.score = score

Student.set_score = set_score
  • 如果想要限制实例的属性,比如只允许添加name和age属性:
1
2
class Student(object):
__slots__ = ('name', 'age')
  • 但是这样只对当前类起作用,子类无效。

使用@property

  • 如果对属性的取值有要求,通常的做法是写成set_xxx函数。
  • 利用@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, value):
if not isinstance(value, int):
raise ValueError('score must be an integer')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100')
self._score = value
  • 如果不定义setter方法,就是一个只读属性。

多重继承

Python允许使用多重继承,MixIn就是一种常见的设计。

定制类

__str__()

重写该函数,可以在print(对象)的时候输出自定义的内容。这是为用户服务的。

__repr__()

重写该函数,可以在交互界面直接执行该对象时输出自定义的内容。这是为开发者服务的。往往应该与上者一致。偷懒的写法是:

1
2
3
4
5
6
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return '%s' % self.name
__repr__ = __str__

__iter__

返回一个迭代对象,以用于for…in循环。for循环会不断地调用__next__()拿到下一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化两个计数器a,b

def __iter__(self):
return self # 实例本身就是迭代对象,故返回自己

def __next__(self):
self.a, self.b = self.b, self.a + self.b # 计算下一个值
if self.a > 100000: # 退出循环的条件
raise StopIteration();
return self.a # 返回下一个值

>>> for n in Fib():
... print(n)

__getitem__()

  • 重写该方法,使之支持下标访问。参数是(self, n)。但是想实现切片、负数和步距,还需要更多代码。比如判断isinstance(n, slice)来处理切片的情况。
  • 如果把对象看成dict, 这个函数的参数还可能是一个可以作为key的object,比如str。
  • __setitem__()方法把对象视作list或者dict来对集合复制。
  • __delitem__()方法用于删除某个元素。

__getattr__()

  • 调用不存在的成员时会触发。默认抛出AttributeError异常。可以复写这个函数来处理某些情况,注意让别的情况继续抛出该异常,否则的话会默认返回None。
  • 作用:可以针对完全动态的情况作调用。比如REST API。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Chain(object):

def __init__(self, path = ''):
self._path = path

def __getattr__(self, path):
return Chain('%s/%s' % (self._path, path))

def __str__(self):
return self._path

__repr__ = __str__

>>> Chain().status.user.timeline.list
'/status/user/timeline/list'

__call__()

  • 把实例看作函数进行调用。
  • callable(object)可以用来判断一个对象是否可以被调用。

使用枚举类

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)
  • 这样就可以直接使用Month.Jan来应用一个常量。
  • value是自动赋给成员的int常量,默认从1开始。
  • 如果需要更精确地控制枚举类型,要派生Enum类:
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
from enum import Enum, unique

@unique
class Weekday(Enum):
Sun = 0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6

>>> print(Weekday.Tue)
>>> print(Weekday['Tue'])
>>> print(Weekday(2))
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> 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
  • @unique装饰器检查重复值。

使用元类

使用type()动态创建类

  • 动态语言本身支持运行期动态创建类。
  • 假设Hello类有一个hello方法,一个实例为h。
  • type(Hello)返回type:
  • type(h)返回class Hello:
  • type()还可以创建出新的对象:
1
2
3
4
def fn(self, name = 'world'):
print('Hello, %s.' % name)

Hello = type('Hello', (object,), dict(hello = fn))

唯一不同的是:print(type(h))输出的是<class '__main__.Hello'>

使用metaclass控制类的创建行为

  • 类似我们要“先定义类,然后创建实例”,我们要“先定义metaclass, 然后创建类”。
1
2
3
4
5
6
7
8
9
10
11
12
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

>>> L = MyList()
>>> L.add(1)
>>> L
[1]
  • 约定类名以Metaclass结尾。
  • 必须从type类型派生。
  • 定义类时传入关键字参数metaclass。
  • __new__()的参数:

    • 准备创建的类的对象;
    • 类的名字;
    • 类继承的父类集合;
    • 类的方法集合。
  • 应用:ORM(Object Relational Mapping),即对象-关系映射。编写一个ORM框架,所有类都只能动态定义。简单实现方式

错误、调试、测试

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
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')
  • else:没有错误时执行。
  • 根据错误类型不同执行不同的except语句,所有错误类型都来自BaseException。
  • 根据继承关系,把更宽泛的错误类型写后面。
  • try…except…finally可以跨越多层调用捕获错误。通过调用堆栈可以找到根源。
  • 在except中使用logging.exception(e)来记录错误而不退出程序。通过配置,可以记录到日志文件。
  • 派生错误类型类,然后用raise语句可以抛出自定义错误。
  • 如果当前try…catch无法处理某个错误,可以交给上级去处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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('ValueError!')
raise

bar()
  • 这里raise如果不带参数,就把错误原样抛出。也可以加参数转换错误类型,只要是合理的转换逻辑。

调试

  • 使用print()。
  • 使用assert:
1
2
3
4
5
6
7
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n

def main():
foo('0')

如果断言失败,就会抛出AssertionError。启动Python编辑器时可以用-O参数来关闭assert(看作pass):

1
python3 -O err.py

  • 使用logging:
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)

使用logging可以指定记录信息的级别:debug、info、warning、error。也可以轻易配置输出到不同位置。

  • 使用pdb:
1
python3 -m pdb err.py

命令1:查看代码
命令n:单步执行
命令p 变量名:查看变量
命令q:结束调试

  • 使用pdb.set_trace()
    import pdb,然后用pdb.set_trace()来设置断点。再使用pdb调试。遇到断点会停下,命令c可以继续运行。

  • 使用IDE,如PyCharm、Eclipse + pydev。固然方便,但最后你会发现,logging才是终极武器。

单元测试

  • 测试驱动开发:TDD、Test-Driven Development。
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
class Dict(dict):
#编写一个可以使用属性来访问的Dict。利用__getattr__和__setattr__。

import unittest

from mydict import Dict

class TestDict(unittest.TestCase):

def test_init(self):
d = Dict(a = 1, b = 'test')
self.assertEqual(d.a, 1)
self.assertEquel(d.b, 'test)
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.assertERqual(d['key'], 'value')

def test_keyerror(self):
d = Dict()
with self.assertRaises(KeyError):
value = d['empty']

def test_attrerror(self):
d = Dict()
with self.assertRaises(AttributeError):
value = d.empty
  • 测试类要从unittest.TestCase继承,测试方法要以test_开头。
  • 主要利用assertEqual和assertRaises来处理情况。
  • 运行方式一:在文件最后加上下面这段当做正常python脚本运行。
1
2
if __name__ == '__main__':
unittest.main()
  • 运行方式二:通过参数:-m unittest
  • 两个特殊的方法:setUp(self)和tearDown(self),相当于构造和析构。
  • 总结:单元测试用例要覆盖常用的输入组合、边界条件、异常。

文档测试

  • Python内置的文档测试模块(doctest)可以提取注释中的代码并执行调试。
  • doctest严格按照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
30
31
32
33
34
35
36
37
38
39
40
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

if __name__=='__main__':
import doctest
doctest.testmod()

IO编程

  • 同步IO、异步IO。
  • 异步IO的性能远高于同步IO,但是编程模型复杂,如需要考虑用回调模式还是轮询模式。

文件读写

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
#一般情况
>>> f = open('/Users/Fanze/test.txt', 'r') #若文件不存在会抛出IOError错误。
>>> f.read() #一次性读取文件的全部内容,适用于小文件。
'Hello, world'
>>> f.read(size) #每次最多读取size个字节的内容。
>>> f.readline() #读取一行内容。
>>> f.readlines() #一次性读取所有内容,按行返回一个list。
>>> f.close()

#错误处理
try:
f = open('path/to/file', 'r')
print(f.read())
finally:
if f:
f.close()

#推荐用法
with open('/path/to/file', 'r') as f:
print(f.read())

#读取二进制文件
>>> f = open('/path/to/file', 'rb') #读取二进制文件。
>>> f.read()
b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' #十六进制表示的字节。

#按指定字符编码打开文件。遇到不规范的文件,会遇到UnicodeDecodeError。设置errors可以处理它。
>>> f = open('/path/to/gbk.txt', 'r', encoding = 'gbk', errors = 'ignore')
>>> f.read()
'测试'
  • 写文件与读文件相同,用’w’替换’r’,用f.write替换f.read。一定要用f.close()否则缓存中的数据可能不会完成写入。

StringIO和BytesIO

StringIO

  • 在内存中读写str。使用write()和getValue()方法。
1
2
3
4
5
6
7
8
>>> from io import StringIO
>>> f = StringIO()
>>> f.write('hello')
5
>>> f.write(' ')
1
>>> print(f.getValue())
hello
  • StringIO对象也可以用str作为参数进行构造,然后像文件一样用readline()等函数读取。

BytesIO

  • 在内存中操作二进制数据。
1
2
3
4
5
6
>>> from io import BytesIO
>>> f = BytesIO()
>>> f.write('中文'.encode('utf-8'))
6
>>> printf(f.getvalue())
b'\xe4\xb8\xad\xe6\x96\x87'
  • 同样的,BytesIO对象也可以用bytes作为参数进行构造,然后像文件对象一样进行操作。

操作文件和目录

  • 使用os模块直接调用操作系统提供的接口函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> import os
>>> os.name
'posix' #posix:Linux、Unix、OS X。nt:Windows。
>>> os.uname() #查看系统的详细信息。但是在windows上不提供。
>>> os.environ #查看系统的环境变量
>>> os.environ.get('PATH') #获取某个环境变量的值
>>> os.environ.get('x', 'default')

#对路径的操作
>>> os.path.abspath('.') #查看绝对路径
>>> os.path.join('/Users/Fanze', 'testDir') #得到完整目录,这是因为Windows下用的是'\',Linux等用的是'/'
'/Users/Fanze/testDir'
>>> os.path.split('/Users/Fanze/testDir/file.txt') #拆分路径字符串。
('/Users/Fanze/testDir', 'file.txt')
>>> os.path.splitext('/path/to/file.txt') #拆分到文件扩展名
('/path/to/file', '.txt')

#对目录的操作
>>> os.mkdir('/Users/Fanze/testDir')
>>> os.rmdir('/Users/Fanze/testDir')

#对文件的操作
>>> os.rename('test.txt', 'test.py')
>>> os.remove('test.py')
  • 复制文件的函数在os模块中不存在,因为这并非是操作系统提供的系统调用。但是可以借助shutil模块中的copyfile(),这个模块可以看做是os的补充。
1
2
3
4
#列出当前目录下的所有目录
>>> [x for x in os.listdir('.') if os.path.isdir(x)]
#列出所有的.py文件
>>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1] == '.py']

序列化(pickling)

  • 在其他语言里一般称之为serialization、marshalling、flatterning。
  • 使用pickle模块.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> import pickle
>>> d = dict(name = 'Bob', age = 20, score = 88)
>>> pickle.dumps(d)
b'\x80\x03}q\x00(X\x03\x00\x00\x00ageq\x01K\x14X\x05\x00\x00\x00scoreq\x02KXX\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.'

#直接写入一个file-like Object:
>>> f = open('dump.txt', 'wb')
>>> pickle.dump(d, f)
>>> f.close()

#读入到内存
>>> f = open('dump.txt', 'rb')
>>> d = pickle.load(f)
>>> f.close()
>>> d
{'age': 20, 'score': 88, 'name': 'Bob'}
#还可以使用pickle.loads()方法从bytes中反序列化出对象。
  • 使用json模块。
1
2
3
4
5
>>> import json 
>>> d = dict(name = 'Bob', age = 20, score = 88)
>>> json.dumps(d)
'{"age": 20, "score": 88, "name": "Bob"}'
#同样的,dump()方法可以直接把JSON写入一个file-like Object。load和loads方法可以读取file-like Object或者字符串并反序列化。
  • JSON标准规定编码是UTF-8,所以我们总是能正确地在Python中实现str和JSON字符串之间进行转换。

  • Python的dict对象序列化为JSON的{},但是class默认是不能序列化的。需要利用dumps()的可选参数default,为他制定类中一个返回dict的方法。如:

1
2
3
4
5
6
7
8
9
10
def student2dict(std):
return {
'name': std.name,
'age': std.age
'score': std.score
}
>>> print(json.dumps(s, default = student2dict))

#利用__dict__属性(有些类例外,比如定义了__slot__),可以有个偷懒的写法:
>>> print(json.dumps(s, default = lambda obj: obj.__dict__))
  • 如果想反序列化成class,要在loads()中传入object_hook函数来把dict转换为object:
1
2
3
def dict2student(d):
return Student(d['name'], d['age'], d['score'])
>>> print(json.loads(json_str, object_hook = dict2student))

进程和线程

多进程

  • fork()函数:调用一次,返回两次。操作系统把当前进程复制一份,父进程里返回子进程的ID,子进程返回0。子进程中可以调用getppid()得到父进程的ID。
  • os模块对fork等进行了封装。包括:os.fork()、os.getpid()、os.getppid()等。
1
2
3
4
5
6
7
import os 
print('Process (%s) start...' % os.getpid())
pid = os.fork()
if pid == 0:
print('I am child (%s) , my parent is (%s)' % (os.getpid(), os.getppid()))
else:
print('I (%s) created a child (%s)' % (os.getpid(), pid))
  • fork只能在Unix/Linux上使用,想要使用跨平台版本的多进程要使用multiprocessing模块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#启动一个子进程并等待其结束
from multiprocessing import Process
import os

def run_proc(name):
print('this is child %s' % name)

if __name__ == '__main__':
print('this is parent')
p = Process(target = run_proc, args = ('test',))
print('child start.')
p.start()
p.join()
print('child end.')
  • 使用进程池(Pool)启动大量子进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from multiprocessing import Pool
import os, time, random

def long_time_task(name):
start = time.tem()
time.sleep(random.random() * 3)
end = time.time()
print('%s : %0.2f seconds.' % (name, (end - start)))

if __name__ == '__main__':
p = Pool(4) #指定最多同时执行的进程数。默认值是CPU的核数。
for i in range(5): #创建5个进程。
p.apply_async(long_time_task, args=(i,))
print('Waiting')
p.close()
p.join() #调用之前必须先调用close,调用close()之后就不能继续添加新的进程了。
print('Done')
  • 使用子进程:子进程一般并不是自身,而是一个外部进程。创建之后还需要控制其输入和输出。使用subprocess模块。
1
2
3
4
5
6
7
8
9
10
11
12
import subprocess

r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r) #返回0

#如果子进程还需要输入,则通过communicate()方法:
import subprocess

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)
  • Python封装了进程间通讯的底层机制,可以使用Queue、Pipies等来交换数据。
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
#创建两个子进程,一个写Queue,一个读Queue。
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__':
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 启动子进程pr,读取:
pr.start()
# 等待pw结束:
pw.join()
# pr进程里是死循环,无法等待其结束,只能强行终止:
pr.terminate()
  • multiprocessing在Windows下模拟实现fork()效果是通过pickle序列化实现的。

多线程

  • Python的线程是真正的Posix Thread,而不是模拟出来的线程。
  • 两个模块:_thread和threading,后者是对前者的封装。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
  • threading.current_thread()永远返回当前线程的实例。主线程为MainThread,子线程在创建时指定(默认是Thread-1、Thread-2…)。
  • 多进程中,每个进程的变量互不影响。多线程中,所有变量由所有线程共享。使用锁来控制冲突:
1
2
3
4
5
6
7
8
9
10
11
12
13
balance = 0
lock = threading.Lock()

def run_thread(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
  • 因为GIL锁(Global Interpreter Lock)的存在,Python无法像其它语言(C、C++、Java)让多线程充分利用多核处理器,除非使用扩展或者重写一个不带GIL的解释器。但是可以通过多进程实现多核任务。
  • GIL:任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

ThreadLocal

  • 让同一个全局变量在每个线程中都有一个副本。常用于每个线程绑定一个数据库连接、HTTP请求、用户身份信息等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import threading

# 创建全局ThreadLocal对象,可以把它看作dict来绑定其它变量:
local_school = threading.local()

def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))

def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()

t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

多进程对比多线程

  • 多进程:稳定,子进程崩溃不影响其它进程。但是创建进程代价大,进程数不能太多。Apache早期采用。
  • 多线程:比多进程稍快。一个线程的崩溃会影响整个进程。Windows下效率比多进程高。IIS采用。

  • 计算密集型任务:需要进行大量计算,消耗CPU资源。同时进行的任务数等同于CPU核心数效率最高,适合用C语言编写。

  • IO密集型任务:大部分时间在等待网络、磁盘IO。CPU消耗很少。在一定限度内,任务越多,CPU效率越高。C和Python运行效率差别不大,应注重开发效率选择Python。

  • 异步IO:用单进程单线程执行多任务,称之为事件驱动模型,Python里称为协程。如Nginx就是支持异步IO的Web服务器。

分布式进程

  • Thread和Process应优选Process。
  • Process可以分布到多台机器上,Thread不行。利用multiprocessing模块中的managers子模块。
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
# task_master.py

import random, time, queue
from multiprocessing.managers import BaseManager

# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()

# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
pass

# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
QueueManager.register('get_task_queue', callable=lambda: task_queue)
QueueManager.register('get_result_queue', callable=lambda: result_queue)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('', 5000), authkey=b'abc')
# 启动Queue:
manager.start()
# 获得通过网络访问的Queue对象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放几个任务进去:
for i in range(10):
n = random.randint(0, 10000)
print('Put task %d...' % n)
task.put(n)
# 从result队列读取结果:
print('Try get results...')
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
29
30
31
32
33
34
35
# task_worker.py

import time, sys, queue
from multiprocessing.managers import BaseManager

# 创建类似的QueueManager:
class QueueManager(BaseManager):
pass

# 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')

# 连接到服务器,也就是运行task_master.py的机器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
# 从网络连接:
m.connect()
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
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 Queue.Empty:
print('task queue is empty.')
# 处理结束:
print('worker exit.')

正则表达式

  • a:精确匹配。
  • \d:匹配一个数字。
  • \w:匹配一个字母或数字。
  • \s:匹配一个空白符。
  • .:匹配任意字符。
  • *:匹配任意个字符。

  • +:匹配至少一个字符。

  • ?:匹配0或1个字符。
  • {n}:匹配n个字符。
  • {n,m}:匹配n~m个字符。

  • [0-9a-zA-Z\_]:匹配一个数字、字母或下划线。

  • [a-zA-Z\_][0-9a-zA-Z\_]*可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串。

  • A|B:匹配A或B。

  • ^\d:必须以数字开头。
  • \d$:必须以数字结束。

  • 强烈建议Python中描述正则表达式的字符串用r'xxxx'

1
2
3
4
5
6
7
8
9
10
11
>>> import re
>>> re.match(r'^\d{3}-\d{3,8}$', '010-12345') //成功,返回Match对象。
<_sre.SRE_Match object; span=(0,9), match='010-12345'>
>>> re.match(r'^\d{3}-\d{3,8}$', '010 12345') //失败,返回None。
>>>

test = 'xxxxxx'
if re.match(r'exp', test):
print('ok')
else:
print('failed')

正则表达式的应用

  • 用于切分字符串。
1
2
3
4
5
6
7
8
9
#正常切分:
>>> 'a b c'.split(' ')
['a', 'b', '', '', 'c']

#使用正则表达式:
>>> re.split(r'\s+', 'a b c')
['a', 'b', 'c']
>>> re.split(r'[\s\,\;]+', 'a,b, c;; d')
['a', 'b', 'c', 'd']
  • 用于分组。使用()
1
2
3
4
5
6
7
8
9
>>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
>>> m
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> m.group(0) #永远是原始字符串
'010-12345'
>>> m.group(1)
'010'
>>> m.group(2)
'12345'
  • 贪婪匹配:匹配尽可能多的字符。默认开启。加?可关闭。
1
2
3
4
5
# 匹配数字后面的0
>>> re.match(r'^(\d+)(0*)$', '102300').groups()
('102300', '')
>>> re.match(r'^(\d+?)(0*)$', '102300').groups()
('1023', '00')
  • 处于效率的考虑,要重复使用的正则表达式可以先进行预编译,使用时也不用再次书写正则表达式:
1
2
3
4
5
6
import re
re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
>>> re_telephone.match('010-12345').groups()
('010', '12345')
>>> re_telephone.match('010-8086').groups()
('010', '8086')

常用内建模块

datetime

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
#当前时间
>>> from datetime import datetime #注意是datetime模块中的datetime类
>>> now = datetime.now() #类型是datetime.datetime
>>> print(now)
2015-05-18 16:28:07.198690

#构造时间
>>> dt = datetime(2015, 4, 19, 12, 20)

#与timestamp互转
>>> dt.timestamp()
1429417200.0 #小数表示毫秒,Java、JS等语言不使用小数,整数值是Python的1000倍。
>>> print(datetime.fromtimestamp(1429417200.0))
2015-04-19 12:20:00 #本地时间,即2015-04-19 12:20:00 UTC+8:00
>>> print(datetime.utcfromtimestamp(1429417200.0))
2015-04-19 04:20:00 #UTC+0:00的时间。

#与str互转
>>> cday = datetime.strptime('2015-6-1 18:19:59', '%Y-%m-%d %H:%M:%S')
>>> print(datetime.now().strftime('%a, %b %d %H:%M'))
Mon, May 05 16:28

#运算
>>> from datetime import timedelta
>>> datetime.now() + timedelta(days = 2, hours = 12)

#强制设置时区属性(默认tzinfo为None)
>>> from datetime import timezone
>>> tz_utc_8 = timezone(timedelta(hours = 8))
>>> datetime.now().replace(tzinfo = tz_utc_8)

#时区转换(拿到一个datetime,要获知其正确的时区,然后强制设置时区,再转换)
>>> utc_dt = datetime.utcnow().replace(tzinfo = timezone.utc)
>>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours = 8)))

collections

namedtuple

  • 把tuple简单封装成一个类(是tuple的子类),限制元素个数,并可通过属性访问。
1
2
3
>>> from collections import namedtuple
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(1, 2)

deque

  • 链表?解决list作为线性存储插入和删除效率低的问题。
1
2
3
4
5
6
>>> from collections import deque
>>> q = deque(['a', 'b', 'c'])
>>> q.append('x')
>>> q.appendleft('y')
>>> q.pop()
>>> q.popleft()

defaultdict

  • 让访问dict中不存在的key时,不抛出KeyError而是返回默认值。
1
2
>>> from collections import defaultdict
>>> dd = defaultdict(lambda: 'N/A') #注意参数是函数

OrderedDict

  • 有序的dict,按Key得插入顺序排列。可用于实现FIFO。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)

Counter

  • 计数器,比如统计字符出现个数:
1
2
3
4
>>> from collections import Counter
>>> c = Counter()
>>> for ch in 'programming':
... c[ch] = c[ch] + 1

base64

  • 把每3字节的二进制数据编码为4字节的文本数据。文本包括['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']
  • 如果末尾有剩余1或2个直接,则补\x00,再在编码末尾加上1或2个=
1
2
3
4
5
>>> import base64
>>> base64.b64encode(b'binary\x00string')
b'YmluYXJ5AHN0cmluZw=='
>>> base64.b64decode(b'YmluYXJ5AHN0cmluZw==')
b'binary\x00string'
  • 因为’+’和’/‘不能在URL中直接作为参数,所以又有一种”url safe”的base64编码,把他们分别替换成’-‘和’_’:
1
2
3
4
5
6
>>> base64.b64encode(b'i\xb7\x1d\xfb\xef\xff')
b'abcd++//'
>>> base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xff')
b'abcd--__'
>>> base64.urlsafe_b64decode('abcd--__')
b'i\xb7\x1d\xfb\xef\xff'
  • 可以使用自定义的编码表,但通常没必要。
  • Base64不能用于加密,即使使用自定义的编码表。
  • Base64适用于小段内容,如数字证书签名、Cookie。
  • 因为=也不能出现在URL中,所以有些地方会把=去掉,可以根据4的倍数的原理推算出。

struct

实现任意类型到bytes的转换。(略,用到时再查)

hashlib

  • 摘要算法:又称哈希算法、散列算法。
1
2
3
4
import hashlib
md5 = hashlib.md5()
md5.update('how to use md5'.encode('utf-8')) //可将字符串分割,再多次调用update。
print(md5.hexdigest())
  • 要使用SHA1算法的话只要把md5换成sha1。

itertools

  • 得到有限或无限的迭代器。(继而用for…in遍历)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> import itertools
>>> natuals = itertools.count(1) //无限的自然数序列
>>> cs = itertools.cycle('ABC') //无限的'ABCABCABC...'
>>> ns = itertools.repeat('A', 3) //'AAA',如果没有第二个参数就是无限的
>>> ns = itertools.takewhile(lambda x: x <= 10, natuals) //从无限的序列中截取一部分
>>> itertools.chain('ABC', 'XYZ') //串联一组迭代对象

#分组
>>> 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']

XML

操作XML的两种方法:

  • DOM:把整个XML读入内存,解析为树。
  • SAX:流模式,边读边解析。需要自己处理事件。主要使用三个事件:start_element,end_element,char_data。
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 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)

HTMLParser

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
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)

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

urllib

GET

  • 抓取GET请求:
1
2
3
4
5
6
7
8
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
Status: 200 OK
Server: nginx
Date: Tue, 26 May 2015 10:02:27 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2049
Connection: close
Expires: Sun, 1 Jan 2006 01:00:00 GMT
Pragma: no-cache
Cache-Control: must-revalidate, no-cache, private
X-DAE-Node: pidl1
Data: {"rating":{"max":10,"numRaters":16,"average":"7.4","min":0},"subtitle":"","author":["廖雪峰编著"],"pubdate":"2007-6","tags":[{"count":20,"name":"spring","title":"spring"}...}
  • 模拟iPhone 6发送GET请求:
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'))

返回:

1
2
3
4
5
...
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0">
<meta name="format-detection" content="telephone=no">
<link rel="apple-touch-icon" sizes="57x57" href="http://img4.douban.com/pics/cardkit/launcher/57.png" />
...

Post

  • 模拟微博登录:
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 urllib import request, parse

print('Login to weibo.cn...')
email = input('Email: ')
passwd = input('Password: ')
login_data = parse.urlencode([
('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')
])

req = request.Request('https://passport.weibo.cn/sso/login')
req.add_header('Origin', '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/login?entry=mweibo&res=wel&wm=3349&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'))

成功:

1
2
3
4
5
6
Status: 200 OK
Server: nginx/1.2.0
...
Set-Cookie: SSOLoginState=1432620126; path=/; domain=weibo.cn
...
Data: {"retcode":20000000,"msg":"","data":{...,"uid":"1658384301"}}

失败:

1
2
...
Data: {"retcode":50011015,"msg":"\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef","data":{"username":"example@python.org","errline":536}}

Handler

比如通过Proxy去访问网站……

常用第三方模块

PIL(Python Imaging Library)

  • PIL只支持到Python 2.7,要使用Pillow兼容3.x。

virtualenv

  • 为不同应用创建隔离的Python环境。

图形界面

  • 原生库:Tkinter(封装了访问Tk的接口)。
  • 第三方库:Tk(使用Tcl语言开发,支持多个OS)、wxWidgets、Qt、GTK等。

网络编程

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
# 导入socket库:
import socket

# 创建一个socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #AF_INET表示ipv4,ipv6使用AF_INET6。
# 建立连接:
s.connect(('www.sina.com.cn', 80))
# 发送数据:
s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')
# 接收数据:
buffer = []
while True:
# 每次最多接收1k字节:
d = s.recv(1024)
if d:
buffer.append(d)
else:
break
data = b''.join(buffer)
# 关闭连接:
s.close()
# 把HTTP头和网页分离,把HTTP头打印出来,网页内容保存到文件:
header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
# 把接收的数据写入文件:
with open('sina.html', 'wb') as f:
f.write(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
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 监听端口:
s.bind(('127.0.0.1', 9999))
# 开始监听:
s.listen(5) #参数是等待连接的最大数目
print('Waiting for connection...')
# 等待接受连接请求:
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)

说明:我们要绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,还可以用127.0.0.1绑定到本机地址。127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,也就是说,外部的计算机无法连接进来。

配套的客户端:

1
2
3
4
5
6
7
8
9
10
11
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('127.0.0.1', 9999))
# 接收欢迎消息:
print(s.recv(1024).decode('utf-8'))
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.send(data)
print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
s.close()

UDP编程:

服务器端:

1
2
3
4
5
6
7
8
9
10
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定端口:
s.bind(('127.0.0.1', 9999))
# 不用调用listen()方法,直接接受来自客户端的数据。
print('Bind UDP on 9999...')
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
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 不需要connect()
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.sendto(data, ('127.0.0.1', 9999))
# 接收数据:
print(s.recv(1024).decode('utf-8'))
s.close()

注意:UDP端口和TCP端口互不冲突。

电子邮件

  • MUA:Mail User Agent,邮件用户代理,如Outlook、Foxmail
  • MTA:Mail Transfer Agent,邮件传输代理
  • MDA:Mail Delivery Agent,邮件投递代理
  • 一封电子邮件的旅程:发件人 -> MUA -> MTA -> MTA -> 若干个MTA -> MDA <- MUA <- 收件人

编程就是为了实现:

  • MUA把邮件发到MTA。(SMTP)
  • MUA从MDA上收邮件。(POP3、IMAP4)