初学Python的时候整理的学习笔记。
常用网址
安装与配置
Python Shell
安装后可在程序菜单中找到IDLE
进入交互帮助模式:help()
退出:quit
库的搜索路径:
1 | import sys |
添加新路径1
sys.path.insert(0, '新路径')
内置数据类型
- Booleans 布尔型:True 或者False。0,空list、tuple、set、dict,None 都为False。
- Numbers 数值型:Integers、Floats、Fractions 分数、Complex Number 复数。
- Strings 字符串:Unicode 字符序列
- Bytes 字节 和 Byte Arrays 字节数组:例如 一份 jpeg 图像文件。用带
b
前缀的单引号或双引号表示,如b'abc'
,每个字符都只占一个字节 - Lists 列表:值的有序序列,变长
- Tuples 元组:值的有序序列,定长
- Sets 集合:装满无序值的包裹。
- Dictionaries 字典:键值对的无序包裹。
常量
Python中,通常用全部大写的变量名表示常量。但是Python根本没有任何机制保证该常量不会被改变,所以,用全部大写的变量名表示常量只是一个习惯上的用法
浮点数
可以用科学计数法,如1
a = 1.2e-5
Python的整数没有大小限制
Python的浮点数也没有大小限制,但是超出一定范围就直接表示为inf(无限大
运算符
- / 浮点除法,结果一律float (在Python 2 中表示整数除法)
- // 古怪的整数除法,向下取整,如 11//2为5, -11//2为-6,如果分子或分母中有float,则结果也取整后的float。否则为整数
- ** 幂
- % 余数
分数
1 | import fractions |
会自动进行约分
list
列表,元素可变。
1 | a_list = ['a', 'b', 'mpilgrim', 'z', 'example'] |
tuple
元组,元素不可变
引用、切片(会创建新tuple)等方法与list相同1
a_tuple = ("a", "b", "mpilgrim", "z", "example") # 也可以用单引号
如果创建单元素元组,需要在值后加一个逗号
好处:
- 元组的速度比列表更快。
- 对不需要改变的数据进行“写保护”将使得代码更加安全。使用元组替代列表就像是有一条隐含的 assert 语句显示该数据是常量,特别的想法(及特别的功能)必须重写。(??)
- 一些元组可用作字典键(特别是包含字符串、数值和其它元组这样的不可变数据的元组)。列表永远不能当做字典键使用,因为列表不是不可变的。
一次赋多值
1 | # 使用元组,下面的括号都可以省略 |
set
1 | # 创建set |
dict
dict是键值对的无序集合。向dict添加一个键的同时,必须为该键增添一个值
1 | # 创建dict |
值为list时1
2
3
4
5
6
7
8
9
10
11
12SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}
# 长度
len(SUFFIXES) # 2
# 检测值
1000 in SUFFIXES
# 查询值
SUFFIXES[1000] # ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
SUFFIXES[1000][3] # 'TB'
None
None 是 Python 的一个特殊常量。 它是唯一的空值。None 与 False 不同。None 不是 0 。None 不是空字符串。将 None 与任何非 None 的东西进行比较将总是返回 False 。
None 是唯一的空值。它有着自己的数据类型(NoneType)。可将 None 赋值给任何变量,但不能创建其它 NoneType 对象。所有值为 None 变量是相等的。
函数
数据类型转换
1 | int(), float(), bool(), str() |
函数别名
把函数名赋给一个变量,相当于给这个函数起了一个“别名”1
2a = abs
a(-1)
定义函数
1 | def my_abs(x): |
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为None
。return None
可以简写为return
。
也可以返回多个值:1
2
3
4
5
6
7
8
9
10
11import math
def move(x, y, step, angle = 0): # angel=0是默认参数,必须写在后
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny
# 调用
x, y = move(100, 100, 60, math.pi / 6)
# 也可以缺少angle
x, y = move(100, 100, 60)
实际上是返回一个tuple
如果想定义一个什么事也不做的空函数,可以用pass
语句。pass
可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass
1
2def nop():
pass
pass
还可以用在其他语句里,比如:1
2
3# 缺少了pass,代码运行就会有语法错误。
if age >= 18:
pass
参数
默认参数
1 | def enroll(name, gender, age=6, city='Beijing'): |
注意1
2
3def add_end(L=[]): # 默认参数指向[],[]会变改变
L.append('END')
return L
这里的默认参数L
也是一个变量,指向对象[]
,每次调用该参数,如果改变了L
所指向对象的内容,则下次调用时,默认参数的内容就变了。上面函数多次执行add_end()
(不传入参数)结果分别为:1
2
3
4['END']
['END', 'END']
['END', 'END', 'END']
....
定义默认参数要牢记一点:默认参数必须指向不变对象!比如None
1
2
3
4
5def add_end(L=None): # 默认参数指向None,None无法被改变
if L is None:
L = []
L.append('END')
return L
可变参数
允许传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 参数前加*号即可,在函数内部,参数numbers接收到的是一个tuple
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
# 调用时可以传入任意个参数
calc()
calc(1)
calc(1,3)
# 把list或tuple的元素变成可变参数,也是加*号
nums = [1, 2, 3]
calc(*nums)
关键字参数
允许传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 两个*号定义
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
# 调用时可以只传入必选参数
person('Michael', 30)
# 也可以传入任意个数的关键字参数
person('Bob', 35, city='Beijing')
person('Adam', 45, gender='M', job='Engineer')
# 可以先组装出一个dict,然后,把该dict转换为关键字参数传进去
# kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra
extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, **extra)
命名关键字参数
要限制关键字参数的名字,就可以用命名关键字参数。
命名关键字参数需要一个特殊分隔符,后面的参数被视为命名关键字参数1
2
3
4
5
6
7# 可以有默认值
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
# 调用时必须传入参数名,否则将报错。
# 和位置参数一样不能省略
person('Jack', 24, city='Beijing', job='Engineer')
*
不是参数,而是特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数
参数组合
可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用,除了可变参数无法和命名关键字参数混合。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数/命名关键字参数和关键字参数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32def 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)
#调用
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}
# 通过一个tuple和dict,也可以调用上述函数
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)的形式调用它,无论它的参数是如何定义的
递归函数
尾递归
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出1
2
3
4
5
6
7
8
9
10
11
12
13
14# 非尾递归
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
# 尾递归:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
循环与迭代Iteration
for…in循环
用于迭代1
2
3names = ['Michael', 'Bob', 'Tracy']
for name in names:
print(name)
对list实现下标循环,enumerate
函数可以把一个list变成索引-元素对:1
2for i, value in enumerate(['A', 'B', 'C']):
print(i, value)
while循环
1 | sum = 0 |
列表生成式
List Comprehensions1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20# 生成[1x1, 2x2, 3x3, ..., 10x10]
[x * x for x in range(1, 11)]
# 加上判断,筛选出仅偶数的平方
[x * x for x in range(1, 11) if x % 2 == 0]
# 使用两层循环,生成全排列
[m + n for m in 'ABC' for n in 'XYZ']
# 使用多个变量
d = {'x': 'A', 'y': 'B', 'z': 'C' }
[k + '=' + v for k, v in d.items()]
# 列出当前目录下的所有文件和目录名
import os #
[d for d in os.listdir('.')]
# 把一个list中所有的字符串变成小写:
L = ['Hello', 'World', 'IBM', 'Apple']
[s.lower() for s in L]
生成器
Python中,一边循环一边计算的机制,称为生成器generator。
generator保存的是算法,用到时才计算。
列表生成式在生成时已经全部计算好。
第一种定义方法
把一个列表生成式的[]改成(),就创建了一个generator1
2
3
4
5
6
7g = (x * x for x in range(10))
# 通过next()函数计算出generator的下一个值,没有更多的元素时,抛出StopIteration的错误。
next(g)
# 也可以用于迭代,此时不需要next()
for n in g
第二种定义方法
如果一个函数定义中包含yield
关键字,那么这个函数就不再是一个普通函数,而是一个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# 函数
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'
# 一个新的例子
def odd():
print('step 1')
yield 1
print('step 2')
yield 3
print('step 3')
yield 5
generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。
用for循环调用这种generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中。接上例:1
2
3
4
5
6
7
8g = fib(6)
while True:
try:
x = next(g)
print('g:', x)
except StopIteration as e:
print('Generator return value:', e.value)
break
迭代器
可以被next()函数调用并不断返回下一个值的对象称为迭代器。
可迭代对象Iterable有两类:
一类是集合数据类型,如list、tuple、dict、set、str等;
一类是generator。
1 | # 判断一个对象是不是可迭代对象,用for in循环迭代 |
生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。可以使用iter()函数把它们变成迭代器1
iter('abc')
为什么list、dict、str等数据类型不是Iterator?
这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
函数式编程
Functional Programming
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言
高阶函数
Higher-order function
变量可以指向函数,函数名就是指向函数的变量。对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数1
2
3
4
5def add(x, y, f):
return f(x) + f(y)
# 调用
add(-5, 6, abs)
map()和reduce()
Python内建了map()和reduce()函数
map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。1
2
3
4
5
6
7def f(x):
return x * x
r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
# r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list
list(r)
# 结果为[1, 4, 9, 16, 25, 36, 49, 64, 81]
reduce函数的参数与map类似。它把一个函数作用在一个可迭代对象Iterable上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算。效果即:reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
1
2
3
4
5
6# 对一个序列求和,其实可以用sum()函数,这里仅用于说明reduce的用法
from functools import reduce
def add(x, y):
return x + y
reduce(add, [1, 3, 5, 7, 9]) # 结果为25
map与reduce一起使用,把str转换为int:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from 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函数进一步简化成
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()
Python内建的filter()函数用于过滤序列。
和map()类似,filter()也接收一个函数和一个序列。和map()不同的时,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。1
2
3
4
5
6
7
8
9
10
11# 只保留奇数
def is_odd(n):
return n % 2 == 1
result = filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15])
# 把一个序列中的空字符串删掉
def not_empty(s):
return s and s.strip()
list(filter(not_empty, ['A', '', 'B', None, 'C', ' ']))
可见用filter()这个高阶函数,关键在于正确实现一个“筛选”函数。
eg:欧拉筛法找出素数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 生成器,从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) # 构造新序列
Iterator是惰性计算的序列,所以我们可以用Python表示“全体自然数”,“全体素数”这样的序列,而代码非常简洁。
sorted()
sorted()函数就可以对list进行排序1
sorted([36, 5, -12, 9, -21])
sorted()函数也是一个高阶函数:1
2
3
4
5# 传入key函数来实现自定义的排序
sorted([36, 5, -12, 9, -21], key=abs)
# 传入reverse=True可以反向排序
sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
返回函数
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# 返回求和函数
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
# 调用
f = lazy_sum(1, 3, 5, 7, 9)
f()
# 每次调用都会返回一个新的函数
f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
f1==f2 # False
函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
闭包的注意点1
2
3
4
5
6
7
8
9
10def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count() # 三个函数执行结果都是9
# 原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9
返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:1
2
3
4
5
6
7
8
9def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs
匿名函数
Python对匿名函数提供了有限支持1
2#计算f(x)=x的平方,直接传入匿名函数:
map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])
关键字lambda表示匿名函数,冒号前面的x表示函数参数
匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,也可以把匿名函数作为返回值返回。
装饰器
本质上,decorator就是一个返回函数的高阶函数。
eg:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 原函数
def now():
print('2015-3-25')
# 定义一个装饰器decorator
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
# 借助Python的@语法,把decorator置于函数的定义处
# 相当于执行了语句:now = log(now)
@log
def now():
print('2015-3-25')
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数1
2
3
4
5
6
7
8
9
10
11
12
13
14# 定义装饰器
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
# 给原函数加上装饰器
# @语句相当于 now = log('execute')(now),两层括号,第一对括号返回装饰器,第二对括号使用装饰器进行包装
@log('execute')
def now():
print('2015-3-25')
仍然存在问题
经过decorator装饰之后的函数,它们的name已经从原来的’now’变成了’wrapper’
一个完整的decorator的写法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import functools
# 不带参数
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
# 带参数
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
偏函数
偏函数Partial function,和数学意义上的偏函数不一样,用于设定参数的默认值(仍然可以传入其他值)。
创建偏函数:new_func = functools.partial(func,*args,**kw)
eg:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# 转换二进制,自定义函数的方法
def int2(x, base=2):
return int(x, base)
# 转换二进制,创建偏函数
import functools
int2 = functools.partial(int, base=2)
# 上述代码实际上固定了int()函数的关键字参数base,相当于:
kw = { 'base': 2 }
int('10010', **kw)
# 如果传入单个值,如
max2 = functools.partial(max, 10)
# 实际上会把10作为*args的一部分自动加到左边,也就是:
max2(5, 6, 7)
# 相当于:
args = (10, 5, 6, 7)
max(*args)
字符串
Python 3,所有的字符串都是使用Unicode编码的字符序列。不再存在以UTF-8或者CP-1252编码的情况。
前缀
以r或R开头(代表raw)的python中的字符串表示(非转义的)原始字符串
以u或U开头的字符串表示unicode字符串
以b或B开头的字符串字节形式表示的字符串bytes
切片
字符串’xxx’也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串1
'ABCDEFG'[::2] # 'ACEG'
编码
详见字符串和编码
计算机系统通用的字符编码工作方式:
- 在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
- 用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件
- 浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器
为了避免乱码问题,应当始终坚持使用UTF-8
编码对str
和bytes
进行转换
编码与解码单个字符
1 | # 字符转整数编码 |
编码与解码字符串
Python的字符串类型是str
,在内存中以Unicode表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str变为以字节为单位的bytes。
反之,如果从网络或磁盘上读取了字节流,那么读到的数据就是bytes。1
2
3
4
5
6
7# str转bytes
# python3中,str没有decode方法
'ABC'.encode('ascii') # b'ABC'
'中文'.encode('utf-8') # b'\xe4\xb8\xad\xe6\x96\x87'
# bytes转str
b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
保存Python源代码
Python源代码也是一个文本文件,所以,当你的源代码中包含中文的时候,在保存源代码时,就需要务必指定保存为UTF-8编码。当Python解释器读取源代码时,为了让它按UTF-8编码读取,我们通常在文件开头写上这两行:1
2#!/usr/bin/env python3
# -*- coding: utf-8 -*-
第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。
字符串长度 len()
1 | # str统计字符个数 |
占位符
常见占位符
|占位符|数据类型|
|–|–|
|%d |整数|
|%f|浮点数|
|%s|字符串|
|%x|十六进制整数|
1 | # 格式化整数和浮点数可以指定是否补0和整数与小数的位数 |
格式化字符串
文档字符串
文档字符串(docstring)也是字符串。当前的文档字符串占用了多行,所以它使用了相邻的3个引号来标记字符串的起始和终止1
2
3
4
5
6
7
8'''Convert a file size to human-readable form.
Keyword arguments:
size -- file size in bytes
a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
if False, use multiples of 1000
Returns: string
'''
复合字段名
Python 3支持把值格式化(format)成字符串。可以有非常复杂的表达式,最基本的用法是使用单个占位符(placeholder)将一个值插入字符串。1
2
3
4>>> username = 'mark'
>>> password = 'PapayaWhip'
>>> "{0}'s password is {1}".format(username, password)
"mark's password is PapayaWhip"
整型替换字段被当做传给format()方法的参数列表的位置索引。即,{0}会被第一个参数替换(在此例中即username),{1}被第二个参数替换(password)
1 | >>> si_suffixes = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] |
{0}代表传递给format()方法的第一个参数,即si_suffixes。注意si_suffixes是一个列表。所以{0[0]}指代si_suffixes的第一个元素,即’KB’。同时,{0[1]}指代该列表的第二个元素,即:’MB’。大括号以外的内容 — 包括1000,等号,还有空格等 — 则按原样输出。语句最后返回字符串为’1000KB = 1MB’。
这个例子说明格式说明符可以通过利用(类似)Python的语法访问到对象的元素或属性。这就叫做复合字段名(compound field names)。以下复合字段名都是“有效的”。
- 使用列表作为参数,并且通过下标索引来访问其元素(跟上一例类似)
- 使用字典作为参数,并且通过键来访问其值
- 使用模块作为参数,并且通过名字来访问其变量及函数
- 使用类的实例作为参数,并且通过名字来访问其方法和属性
以上方法的任意组合
为了使你确信的确如此,下面这个样例就组合使用了上面所有方法:1
2
3
4>>> import humansize
>>> import sys
>>> '1MB = 1000{0.modules[humansize].SUFFIXES[1000][0]}'.format(sys)
'1MB = 1000KB'
下面是描述它如何工作的:
sys
模块保存了当前正在运行的Python实例的信息。由于已经导入了这个模块,因此可以将其作为format()
方法的参数。所以替换域{0}
指代sys
模块。
sys.modules
是一个保存当前Python实例中所有已经导入模块的字典。模块的名字作为字典的键;模块自身则是键所对应的值。所以{0.modules}
指代保存当前己被导入模块的字典。
替换域{0.modules[humansize]}指代humansize模块。
请注意以上两句在语法上轻微的不同。在实际的Python代码中,字典sys.modules的键是字符串类型的;为了引用它们,我们需要在模块名周围放上引号(比如 ‘humansize’)。但是在使用替换域的时候,我们在省略了字典的键名周围的引号(比如 humansize)。在此,我们引用PEP 3101:字符串格式化高级用法,“解析键名的规则非常简单。如果名字以数字开头,则它被当作数字使用,其他情况则被认为是字符串。”
sys.modules[‘humansize’].SUFFIXES是在humansize模块的开头定义的一个字典对象。{0.modules[humansize].SUFFIXES}即指向该字典。
sys.modules[‘humansize’].SUFFIXES[1000]是一个si(国际单位制)后缀列表:[‘KB’, ‘MB’, ‘GB’, ‘TB’, ‘PB’, ‘EB’, ‘ZB’, ‘YB’]。所以替换域{0.modules[humansize].SUFFIXES[1000]}指向该列表。
sys.modules[‘humansize’].SUFFIXES[1000][0]即si后缀列表的第一个元素:’KB’。因此,整个替换域{0.modules[humansize].SUFFIXES[1000][0]}最后都被两个字符KB替换。
格式说明符
1 | if size < multiple: |
{0:.1f}中的:.1f则不一定了。第二部分(包括冒号及其后边的部分)即格式说明符(format specifier),它进一步定义了被替换的变量应该如何被格式化。
☞格式说明符的允许你使用各种各种实用的方法来修饰被替换的文本,就像C语言中的printf()函数一样。我们可以添加使用零填充(zero-padding),衬距(space-padding),对齐字符串(align strings),控制10进制数输出精度,甚至将数字转换成16进制数输出。
在替换域中,冒号(:)标示格式说明符的开始。“.1”的意思是四舍五入到保留一们小数点。“f”的意思是定点数(与指数标记法或者其他10进制数表示方法相对应)。因此,如果给定size为698.24,suffix为’GB’,那么格式化后的字符串将是’698.2 GB’,因为698.24被四舍五入到一位小数表示,然后后缀’GB’再被追加到这个串最后。1
2>>> '{0:.1f} {1}'.format(698.24, 'GB')
'698.2 GB'
模块与包
模块(Module):一个.py文件
包(package):一个目录
相同名字的函数和变量完全可以分别存在不同的模块中,但是尽量不要与内置函数名字冲突。
Python的所有内置函数
每一个包目录下面都会有一个__init__.py
的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py
可以是空文件,也可以有Python代码。无论一个包的哪个部分被导入,
import os
系统在导入模块时,要做以下三件事:
1.为源代码文件中定义的对象创建一个名字空间,通过这个名字空间可以访问到模块中定义的函数及变量。
2.在新创建的名字空间里执行源代码文件.
3.创建一个名为源代码文件的对象,该对象引用模块的名字空间,这样就可以通过这个对象访问模块中的函数及变量
import os as system
模块导入时可以使用 as 关键字来改变模块的引用对象名字。
from socket import gethostname
使用from语句可以将模块中的对象直接导入到当前的名字空间。 from语句不创建一个到模块名字空间的引用对象,而是把被导入模块的一个或多个对象直接放入当前的名字空间。可以使用星号代表模块中除下划线开头的所有对象。不过,如果一个模块如果定义有列表__all__
,则from module import 语句只能导入__all__
列表中存在的对象。
import 语句可以在程序的任何位置使用,你可以在程序中多次导入同一个模块,但模块中的代码仅仅在该模块被首次导入时执行。后面的import语句只是简单的创建一个到模块名字空间的引用而已。sys.modules字典中保存着所有被导入模块的模块名到模块对象的映射。这个字典用来决定是否需要使用import语句来导入一个模块的最新拷贝。
from module import 语句只能用于一个模块的最顶层。特别注意*:由于存在作用域冲突,不允许在函数中使用from 语句。
每个模块都拥有__name__
属性,它是一个内容为模块名字的字符串。最顶层的模块名称是__main__
。命令行或是交互模式下程序都运行在__main__
模块内部。利用__name__
属性,我们可以让同一个程序在不同的场合(单独执行或被导入)具有不同的行为,象下面这样做:1
2
3
4
5
6
7# 检查是单独执行还是被导入
if __name__ == '__main__':
# Yes
statements
else:
# No (可能被作为模块导入)
statements
可以被 import 语句导入的模块共有以下四类:
- 使用Python写的程序( .py文件)
- C或C++扩展(已编译为共享库或DLL文件)
- 包(包含多个模块)
- 内建模块(使用C编写并已链接到Python解释器内)
当查询模块 foo 时,解释器按照 sys.path 列表中目录顺序来查找以下文件(目录也是文件的一种):
1.定义为一个包的目录 foo
2.foo.so, foomodule.so, foomodule.sl,或 foomodule.dll (已编译扩展)
3.foo.pyo (只在使用 -O 或 -OO 选项时)
4.foo.pyc
5.foo.py
对于.py文件,当一个模块第一次被导入时,它就被汇编为字节代码,并将字节码写入一个同名的.pyc文件。后来的导入操作会直接读取.pyc文件而不是.py文件。(除非.py文件的修改日期更新,这种情况会重新生成.pyc文件) 在解释器使用 -O 选项时,扩展名为.pyo的同名文件被使用。pyo文件的内容去掉行号、断言、及其他调试信息的字节码,体积更小,运行速度更快。如果使用-OO选项代替-O,则文档字符串也会在创建.pyo文件时也被忽略。
如果在sys.path提供的所有路径均查找失败,解释器会继续在内建模块中寻找,如果再次失败,则引发 ImportError 异常。
.pyc和.pyo文件的汇编,当且仅当import 语句执行时进行。
当 import 语句搜索文件时,文件名是大小写敏感的。即使在文件系统大小写不敏感的系统上也是如此(Windows等)。
reload(sys)
如果更新了一个已经用import语句导入的模块,内建函数reload()可以重新导入并运行更新后的模块代码。在reload()运行之后的针对模块的操作都会使用新导入代码,不过reload()并不会更新使用旧模块创建的对象,因此有可能出现新旧版本对象共存的情况。
注意 使用C或C++编译的模块不能通过 reload() 函数来重新导入。记住一个原则,除非是在调试和开发过程中,否则不要使用reload()函数。
无论一个包的哪个部分被导入,在文件__init__.py
中的代码都会运行。导入过程遇到的所有 __init__.py
文件都被运行。
from Graphics.Primitive import *
这个语句的原意图是想将Graphics.Primitive包下的所有模块导入到当前的名称空间。然而,由于不同平台间文件名规则不同(比如大小写敏感问题), Python不能正确判定哪些模块要被导入。这个语句只会顺序运行 Graphics 和 Primitive 文件夹下的__init__.py
文件。__init__.py
中定义一个名字all的列表。
这和导入模块不同,类似语句可以正常导入模块下的所有函数。
下面这个语句只会执行Graphics目录下的__init__.py
文件,而不会导入任何模块: import Graphics。不过既然 import Graphics 语句会运行 Graphics 目录下的 __init__.py
文件,只需要在其中把所有模块都import进去就行了。
sys.modules包含了当前所load的所有的modules的dict(其中包含了builtin的modules)
Python模块的标准文件模板:1
2
3
4
5
6#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释 '
__author__ = '作者名'
通过_
前缀定义作用域:
- 正常的函数和变量名是公开的(public),可以被直接引用。
__xxx__
:特殊变量,可以被直接引用,但是有特殊用途。模块定义的文档注释也可以用特殊变量__doc__
访问,我们自己的变量一般不要用这种变量名_xxx
和__xxx
:非公开(private),只是一个命名习惯,外部仍然可以访问
Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。
安装第三方模块
安装Python时确保勾选pip
和Add python.exe to Path
。
一般来说,第三方库都会在Python官方的pypi.python.org网站注册,要安装一个第三方库,必须先知道该库的名称,可以在官网或者pypi上搜索。
以安装Python Imaging Library
为例,这是Python下非常强大的处理图像的工具库。
运行命令:pip install Pillow
即可。
简单用法:1
2
3
4
5from PIL import Image
im = Image.open("d:\\backup\\140591\\桌面\\首页图片\\11.jpg")
print(im.format, im.size, im.mode)
im.thumbnail((200, 100)) # 缩小图片
im.save('22.jpg', 'JPEG') # 保存
其他常用的第三方库还有MySQL的驱动:mysql-connector-python
,用于科学计算的NumPy库:numpy
,用于生成文本的模板工具Jinja2
,等等。
当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件。默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中。
要添加自己的搜索目录,有两种方法:
- 直接修改sys.path,添加要搜索的目录:
sys.path.append('/Users/michael/my_py_scripts')
。这种方法是在运行时修改,运行结束后失效。- 设置环境变量
PYTHONPATH
。
sys
sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称。
运行python3 hello.py
获得的sys.argv
就是['hello.py']
。
在命令行直接运行一个模块文件时,Python解释器把一个特殊变量__name__
置为`main`,注意带了引号。如果是import一个模块,则此变量值为模块文件名,不带路径或扩展名。
面向对象编程
Object Oriented Programming,简称OOP。
类和实例
面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。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
102
103
104
105
106
107
108
109
110
111
112# 定义
class Student(object):
# 限制该class实例在运行期间能添加的属性,可选。对子类无效
__slots__ = ('name', 'age')
# 此处定义类属性
count = 0
# __init__方法的第一个参数永远是self,表示创建的实例本身
# 此处定义实例属性
def __init__(self, name, score):
self.name = name
self.score = score
# 使此class可以适用于系统的len()函数,len('king'),len(obj)...可选
def __len__(self):
# 自己实现
# 类似Java的toString方法,打印对象时调用。可选。
# 返回用户看到的字符串
def __str__(self):
return 'Student object (name: %s)' % self.name
# 在解释器中直接输变量,会调用此__repr__()方法。可选
# 返回程序开发者看到的字符串,此处是偷懒的写法,使两者一样。
__repr__ = __str__
# 使此类的对象可以被用于for ... in循环。可选
# 返回一个迭代对象,该迭代对象必须实现__next__()方法,迭代结束时raise StopIteration()
def __iter__(self):
# 自定义实现
# 使此类的对象可以像list那样按照下标取出元素,可选。
# 如果把对象看成dict,__getitem__()的参数也可能是一个可以作key的object,此处未实现
# 与此对应的是__setitem__()方法,把对象视作list或dict来对集合赋值;__delitem__()方法,删除某个元素
def __getitem__(self, n):
if isinstance(n, int): # n是索引
# 取出对象...
if isinstance(n, slice): # n是切片,slice表示切片对象
start = n.start
stop = n.stop
if start is None:
start = 0
# 取出list...注意要对负数、步长参数作处理
# 调用的属性或方法在类中不存在时,会在此处找,可选。
# 默认返回None
def __getattr__(self, attr):
if attr=='sex':
return 'man' # 返回属性
if attr=='hisage': # 仅作示范,不用在意名字
return lambda: 25 # 返回函数
raise AttributeError('自定义错误信息') # 抛出Error,不再返回默认的None
# 直接把实例当成方法用,可选。
# 一般用instance.method()的形式调用方法,但是定义了此方法后,可以用instance(),如:Student('Michael')()。
# 可添加参数
def __call__(self):
print('My name is %s.' % self.name)
# 其他方法也和__init__类似
def print_score(self):
print('%s: %s' % (self.__name, self.__score))
# @property装饰器把一个getter方法变成属性
# 此处@property又创建了另一个装饰器@score.setter
@property
def score(self):
return self._score
# 如果不设置此set方法,则为只读属性
@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
#################---使用----#############
# 创建实例
s = Student('Bart Simpson', 59)
# score属性的set与get
s.score = 60 # 实际转化为s.set_score(60)
s.score # 实际转化为s.get_score()
# 绑定属性。对象名改成Class名即可对Class绑定
s.age = 8 # 注意class中并没有定义age变量
# 删除属性
del s.name
# 绑定方法,注意区别。对象名改成Class名即可对Class绑定
# 第一种方法:
def nono():
print('nono')
s.nono = nono
s.nono()
# 第二种方法:
def nono(self):
print('nono')
from types import MethodType
s.nono = MethodType(nono, bart)
s.nono()
# 例外:slots只能限制添加属性,不能限制通过添加方法来添加属性:
# 绑定下面的方法即可绕过slots限制
def set_city(self, city):
self.city=city
class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的。通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
有了__init__
方法,在创建实例的时候,必须传入与__init__
方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去。
重点提一下__getattr__
动态调用的用法。例如很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似http://api.server/user/timeline/list
。如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。利用完全动态的__getattr__
,我们可以写出一个链式调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# 定义
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
对象:1
2
3
4
5
6callable(Student()) # 对于类来说,必须实现__call__()才能当函数用
callable(max) # True
callable([1, 2, 3]) # False
callable(None) # False
callable('str') # False
callable(str) # True
访问控制
实例的变量名如果以__
开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。例外:__xxx__
是特殊变量,特殊变量是可以直接访问的,不是private变量。
一个下划线开头的实例变量名_xxx
,外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__xxx
是因为Python解释器对外把__xxx
变量改成了_类名__xxx
,所以,仍然可以通过_类名__xxx
来访问该变量。不同版本的Python解释器可能会把__name改成不同的变量名。
###继承和多态
多态的好处:调用方只管调用,不管细节。这就是著名的开闭原则:对扩展开放,对修改封闭。
静态语言 vs 动态语言
举例说明:1
2def run_twice(animal):
animal.run()
对于静态语言(例如Java)来说,上述代码传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了。
获取对象信息
1 | # 对象类型,使用type()返回对应的Class类型 |
Python中,如果调用len()
函数试图获取一个对象的长度,实际上,在len()
函数内部,它自动去调用该对象的__len__()
方法。自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()
方法
要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如:1
2
3
4def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
上述代码从文件流fp中读取图像,首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。
多重继承
在设计类的继承关系时,通常,主线都是单一继承下来的。但是,如果需要“混入”额外的功能,通过多重继承就可以实现。这种设计通常称之为MixIn。
eg:
Python自带了TCPServer
和UDPServer
这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn
和ThreadingMixIn
提供。通过组合,我们就可以创造出合适的服务来。1
2
3
4
5
6
7
8
9
10
11# 编写一个多进程模式的TCP服务
class MyTCPServer(TCPServer, ForkingMixIn):
pass
# 编写一个多线程模式的UDP服务
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
# 如果你打算搞一个更先进的协程模型,可以编写一个CoroutineMixIn:
class MyTCPServer(TCPServer, CoroutineMixIn):
pass
这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。
枚举类
1 | # 定义方法一 |
元类
1 | #### type()查看类或变量的类型 |
str类用来创建字符串对象
int类用来创建整数对象
type类用来创建类对象
Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。
除此之外还可以用metaclass动态创建类。
eg,给自定义的MyList添加add方法:1
2
3
4
5
6
7
8
9
10
11
12# metaclass是类的模板,所以必须从`type`类型派生:
class ListMetaclass(type): # 默认习惯,metaclass的类名总是以Metaclass结尾
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
# 使用ListMetaclass来定制类
class MyList(list, metaclass=ListMetaclass): # 传入关键字参数metaclass
pass
# Python解释器在创建MyList时,找到metaclass关键字就会通过元类的__new__()来创建类。
# __new__()的参数:新类的对象、新类的名字、新类继承的父类集合、新类的属性方法集合
创建类的过程:
- 当前类中寻找
__metaclass__
属性 - 在父类中寻找该属性
- 模块层次中寻找该属性
- 用内置的type来创建这个类对象。
前三步中只要找到就用它创建类。
模块中添加__metaclass__
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21'''模块中添加__metaclass__'''
# 元类会自动将你通常传给'type'的参数作为自己的参数传入
def upper_attr(future_class_name, future_class_parents, future_class_attr):
'''返回一个类对象,将属性都转为大写形式'''
# 选择所有不以'__'开头的属性
attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
# 将它们转为大写形式
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
# 通过'type'来做类对象的创建
return type(future_class_name, future_class_parents, uppercase_attr)
'''注意这条语句的位置,它属于整个模块'''
__metaclass__ = upper_attr # 这会作用到这个模块中的所有类
class Foo(object):
# 我们也可以只在这里定义__metaclass__,这样就只会作用于这个类中
bar = 'bip'
hasattr(Foo, 'bar') # 输出: False
hasattr(Foo, 'BAR') # 输出:True
类定义中添加__metaclass__
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 沿用上面的代码
# 必须传入type或子类
class UpperAttrMetaClass(type):
# __new__ 是在__init__之前被调用的特殊方法
# __new__是用来创建对象并返回之的方法
# 而__init__只是用来将传入的参数初始化给对象
# 你很少用到__new__,除非你希望能够控制对象的创建
# 这里,创建的对象是类,我们希望能够自定义它,所以我们这里改写__new__
# 如果你希望的话,你也可以在__init__中做些事情
# 还有一些高级的用法会涉及到改写__call__特殊方法,但是我们这里不用
def __new__(cls, name, bases, dct):
attrs = ((name, value) for name, value in dct.items() if not name.startswith('__')
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
# 复用type.__new__方法
return type.__new__(cls, name, bases, uppercase_attr)
# 最后一句也可以使用super
return super(UpperAttrMetaclass, cls).__new__(cls, name, bases, uppercase_attr)
metaclass修改类定义的典型的例子:ORM,全称Object Relational Mapping,即对象-关系映射。就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。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# 定义一个User类来操作对应的数据库表User
class User(Model):
# 定义类的属性到列的映射:
# 父类Model和属性类型StringField、IntegerField是由ORM框架提供的
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 创建一个实例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到数据库:
u.save()
# 输出如下:
'''
Found model: User
Found mapping: email ==> <StringField:email>
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:uid>
Found mapping: name ==> <StringField:username>
SQL: insert into User (password,email,username,id) values (?,?,?,?)
ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]
'''
'''-----------上面是此ORM框架的使用,下面是定义-----------'''
# 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 StringField(Field):
def __init__(self, name):
super(StringField, self).__init__(name, 'varchar(100)')
class IntegerField(Field):
def __init__(self, name):
super(IntegerField, self).__init__(name, 'bigint')
# 编写ModelMetaclass
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# 先排除对Model类对象的修改,则此元类只会影响User类对象的创建
if name=='Model':
return type.__new__(cls, name, bases, attrs)
# 显示正在修改的类,调试用
print('Found model: %s' % name)
# 用于存储Field属性映射
mappings = dict()
# 遍历类的所有属性,把所有Field属性保存到mappings中
# 字典attrs中保存属性名称和属性值的映射,如id与IntegerField('id')。
for k, v in attrs.items():
if isinstance(v, Field):
# 调试用的输出语句
print('Found mapping: %s ==> %s' % (k, v))
# 存储符合条件的Filed属性
mappings[k] = v
# 从类属性中删除该Field属性
for k in mappings.keys():
attrs.pop(k)
# 保存属性和列的映射关系
attrs['__mappings__'] = mappings
# 把表名保存到__table__中,假设表名和类名一致
attrs['__table__'] = name
return type.__new__(cls, name, bases, attrs)
# 基类Model,可以定义各种操作数据库的方法,比如save(),delete(),find(),update()等等
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():
# 这里的v是指字典中的value,阅读前面的代码可知是个Field的子类的实例
# 该实例一个名为name的属性,创建对象时传入
# 如StringField('username'),此处`username`即是name属性
fields.append(v.name)
params.append('?')
args.append(getattr(self, k, None))
# 'xxx'.join(alist)用xxx为分隔符把列表alist连接成字符串
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))
错误、调试和测试
try catch
1 | try: |
Python中错误类型都继承自BaseException
。
常见的错误类型和继承关系:https://docs.python.org/3/library/exceptions.html#exception-hierarchy
抛出错误
1 | class FooError(ValueError): |
调试
断言assert
1 | def foo(s): |
启动解释器时可以用-O
参数来关闭assert:python3 -O err.py
。注意是大写的英文字母O。
调试器pdb
启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。
第一种方法,命令行启动:python3 -m pdb 调试的文件.py
此时输入小写字母l
查看文件内容。n
单步执行代码。p 变量名
查看变量q
结束调试。
第二种方法1
2
3
4
5import pdb
# 在需要暂停的地方插入此代码
# 程序运行到这里会暂停并进入pdb调试环境
pdb.set_trace()
仍然用命令行运行文件python3 调试的文件.py
,调试完c
继续运行
用IDE调试
PyCharm
Eclipse加上pydev插件
单元测试 unittest模块
以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
开发一个Dict类,mydict.py:1
2
3
4
5
6
7
8
9
10
11
12
13class 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
单元测试模块,包含测试类,mydict_test.py: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
51import unittest
from mydict import Dict
# 测试类继承unittest.TestCase
class TestDict(unittest.TestCase):
# 调用测试方法前执行,可用于打开资源如数据库
def setUp(self):
pass
# 调用测试方法后执行
def tearDown(self):
pass
# 以test开头的方法就是测试方法,测试时会被自动执行
def test_init(self):
d = Dict(a=1, b='test')
self.assertEqual(d.a, 1)
self.assertEqual(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.assertEqual(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
# 用于运行单元测试
if __name__ == '__main__': # 直接运行此模块时,判断条件成立
unittest.main() # 注意是包名和方法
# 如果不加上述代码,则需要用如下命令运行:
# python3 -m unittest mydict_test
# mydict_test是此文件名
# 推荐用此方法
文档测试 doctest模块
Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用…表示中间一大段烦人的输出。
还可以直接作为示例代码。通过某些文档生成工具,就可以自动把包含doctest的注释提取出来。用户看文档的时候,同时也看到了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
39# mydict.py
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()
运行模块python3 mydict.py
,没有任何输出说明doctest运行正确
IO编程
同步和异步IO的区别就在于是否等待IO执行的结果。等待=同步IO。
本章只介绍同步IO。异步IO复杂度太高暂略过。
文件读写
Python内置了读写文件的函数,用法和C是兼容的。
在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。
1 | # 打开文件,r表示read,w为write,a为append,b为binary |
捕捉异常:1
2
3
4
5
6
7
8
9
10try:
f = open('/path/to/file', 'r')
print(f.read())
finally:
if f:
f.close()
# 等效的简单写法,不必调用close()
with open('/path/to/file', 'r') as f:
print(f.read())
有个read()方法的对象,在Python中统称为file-like Object
StringIO和BytesIO
StringIO:在内存中读写str1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24from io import StringIO
f = StringIO()
# 写入
f.write('hello')
f.write(' ')
f.write('world!')
# 获得写入后的str
f.getvalue() # hello world!
####----------------------------------
from io import StringIO
# 用一个str初始化StringIO
f = StringIO('Hello!\nHi!\nGoodbye!')
# 读取
while True:
s = f.readline()
if s == '':
break
print(s.strip())
BytesIO:在内存中读写bytes1
2
3
4
5
6
7
8
9from io import BytesIO
f = BytesIO()
f.write('中文'.encode('utf-8')) # 写入UTF-8编码的bytes
f.getvalue()
# 用一个bytes初始化BytesIO
f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
# 读取
f.read()
操作文件和目录
os模块,代表 操作系统(operating system),包含非常多的函数用于获取(和修改)本地目录、文件进程、环境变量等的信息。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
68import os
# 操作系统的类型
os.name
'''
posix ====> Linux、Unix或Mac OS X
nt ====> Windows系统
'''
# 环境变量
os.environ
'''
environ({'PSMODULEPATH': 'C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules\\', 'COMMONPROGRAMW6432': 'C:\\Program Files\\Common Files'.....
'''
# 获取某个环境变量的值
os.environ.get('PATH')
os.environ.get('x', 'default')
s = os.getenv('PATH')
#################################################
# -------------------目录操作-------------------#
#################################################
# 获取当前工作目录 get current working directory
os.getcwd()
# 改变当前目录 change directory, 可以用相对路径
os.chdir('/Users/pilgrim/diveintopython3/examples')
# 查看当前工作目录的绝对路径:
os.path.abspath('.')
# 拼接路径,此方法可以正确处理不同操作系统的路径分隔符
# 合并、拆分路径的函数并不要求目录和文件要真实存在,它们只对字符串进行操作
os.path.join('F:\\PythonWorkspace', 'NewDir')
# expanduser函数用来将包含~符号(表示当前用户Home目录)的路径扩展为完整的路径
os.path.expanduser("~/.pythonrc")
# eg:
os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py')
# 创建新目录
os.mkdir('F:\\PythonWorkspace\\NewDir')
# 删除目录:
os.rmdir('F:\\PythonWorkspace\\NewDir')
# 拆分路径,此方法可以正确处理不同操作系统的路径分隔符
os.path.split('F:\\PythonWorkspace\\file.txt') # ('F:\\PythonWorkspace', 'file.txt')
# 获取文件扩展名
os.path.splitext('F:\\PythonWorkspace\\file.txt') # ('F:\\PythonWorkspace\\file', '.txt')
# 重命名文件:
os.rename('test.txt', 'test.py')
# 删掉文件:
os.remove('test.py')
# os模块中不存在复制文件的函数,原因是复制文件并非由操作系统提供的系统调用。可以使用shutil模块的copyfile()函数
# 列出当前工作目录下的所有目录
# listdir()只返回文件名,不包含完整路径
# 不在当前目录下时,isdir必须加上路径,如果只传入文件夹名字,它会在当前工作目录下搜索是否存在此文件夹
[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']
获取文件元信息
元信息: 创建时间,最后修改时间,文件大小等等1
2
3
4
5
6
7
8
9
10
11import os
import time
# os.stat() 函数返回一个包含多种文件元信息的对象
metadata = os.stat('feed.xml')
# 最后修改时间,从纪元(1970年1月1号的第一秒钟)到现在的秒数
metadata.st_mtime
# time.localtime() 函数将从纪元到现在的秒数转换成包含年、月、日、小时、分钟、秒的结构体。
time.localtime(metadata.st_mtime)
罗列目录内容
glob 模块是Python标准库中的另一个工具,它可以通过编程的方法获得一个目录的内容,并且它使用熟悉的命令行下的通配符。
1 | import glob |
序列化
pickle模块
1 | import pickle |
JSON
JSON标准规定JSON编码是UTF-8
JSON表示的对象就是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:
JSON类型 | Python类型 |
---|---|
{} | dict |
[] | list |
“string” | str |
1234.56 | int或float |
true/false | True/False |
null | None |
Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import json
d = dict(name='Bob', age=20, score=88)
# 序列化
temp = json.dumps(d)
# 和pickle类似,也可以直接把对象序列化后写入一个file-like Object
f = open('dump.txt', 'wb')
json.dump(d, f)
f.close()
# 反序列化
json.loads(temp)
# 也可以直接从一个file-like Object中直接反序列化出对象
f = open('dump.txt', 'rb')
d = json.load(f)
f.close()
除了第一个必须的obj参数外,dumps()方法还提供了一大堆的可选参数:
https://docs.python.org/3/library/json.html#json.dumps
可选参数default用于把任意一个对象变成一个可序列为JSON的对象。
Json序列化类实例的例子: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
32import json
# 类
class Student(object):
def __init__(self, name, age, score):
self.name = name
self.age = age
self.score = score
s = Student('Bob', 20, 88)
# 用于序列化Student类的方法
def student2dict(std):
return {
'name': std.name,
'age': std.age,
'score': std.score
}
# 序列化
json_str = json.dumps(s, default=student2dict)
# 用于反序列化Student类的方法
def dict2student(d):
return Student(d['name'], d['age'], d['score'])
# 反序列化
json.loads(json_str, object_hook=dict2student)
# 通用的序列化方法
# 通常class的实例都有一个__dict__属性,它就是一个dict,用来存储实例变量。也有少数例外,比如定义了__slots__的class。
json.dumps(s, default=lambda obj: obj.__dict__)
进程和线程
多进程 multiprocessing
启动单个进程
Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
*nix系统的创建多进程方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import os
print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
'''
运行结果如下:
Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.
'''
Windows中可以用跨平台的multiprocessing
多进程模块
1 | from multiprocessing import Process |
进程池 类 Pool
批量创建子进程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
44from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('运行任务 %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('任务 %s 运行了 %0.2f 秒。' % (name, (end - start)))
if __name__=='__main__':
print('父进程 %s' % os.getpid())
# 创建4个子进程,编号从0开始
p = Pool(4)
# 执行5个任务
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('等待所有子进程运行结束')
# 调用join()之前必须先调用close()
# close()之后就不能继续添加新的Process了
p.close()
# 等待所有子进程执行完毕
p.join()
print('所有进程运行完毕')
'''
用Sublime Text和Python解释器执行都没有得到正确的结果
命令行执行结果如下:
父进程 2372
等待所有子进程运行结束
运行任务 0 (5268)...
运行任务 1 (5544)...
运行任务 2 (6008)...
运行任务 3 (3732)...
任务 2 运行了 0.19 秒。
运行任务 4 (6008)...
任务 0 运行了 0.38 秒。
任务 1 运行了 1.31 秒。
任务 3 运行了 1.84 秒。
任务 4 运行了 5.75 秒。
所有进程运行完毕
'''
# 由于线程池中只有4个线程,第5个任务(即任务4)必须等其他任务执行结束,才能被空闲的进程执行
子进程 模块 subprocess
相对父进程来说,子进程是一个外部进程。
subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
eg:Python代码中运行命令,这和命令行直接运行的效果是一样的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import subprocess
# 仅运行命令
# 这一条命令能直接在SublimeText控制台输出结果,不知道为什么
# ping
r = subprocess.call(['ping', 'www.python.org'])
# 调用 cmd 执行后面的命令,输出path环境变量
r = subprocess.call(['cmd', '/c', 'echo', '%path%'])
print('Exit code:', r)
# 向子进程输入数据
# 相当于运行命令:
# adb shell
# adb help
# exit
p = subprocess.Popen(['adb shell'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'adb help\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)
进程间通信
Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
1 | from multiprocessing import Process, Queue |
多线程
Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:
_thread
低级模块threading
高级模块,对_thread进行了封装。
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import time, threading
# 新线程执行的代码:
def loop():
# current_thread()返回当前线程的实例
print('thread %s is running...' % threading.current_thread().name)
# ... 做一些事
print('thread %s ended.' % threading.current_thread().name)
# name参数指定子线程的名字
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
# 输出CPU核心数量
print(multiprocessing.cpu_count())
主线程实例的名字叫MainThread,子线程的名字在创建时指定。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……
1 | # 创建锁 |
Python解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
ThreadLocal
一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。
1 | import threading |
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
进程 vs. 线程
多任务,通常用Master-Worker模式,Master负责分配任务,Worker负责执行任务。
多进程模式最大的优点就是稳定性高,缺点是创建进程的代价大(特别是Windows下),操作系统能同时运行的进程数也是有限的。
多线程模式通常比多进程快一点,但是也快不到哪去,而且任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
在Windows下,多线程的效率比多进程要高。
操作系统在切换进程或者线程时,需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),再把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单进程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。我们会在后面讨论如何编写协程。
分布式进程
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52import random, time, queue
from multiprocessing import freeze_support
from multiprocessing.managers import BaseManager
# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()
# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
pass
def return_task_queue():
global task_queue
return task_queue
def return_result_queue():
global result_queue
return result_queue
def test():
# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
# QueueManager.register('get_task_queue', callable=lambda: task_queue)
# QueueManager.register('get_result_queue', callable=lambda: result_queue)
QueueManager.register('get_task_queue', callable=return_task_queue)
QueueManager.register('get_result_queue', callable=return_result_queue)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('127.0.0.1', 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.')
if __name__ == '__main__':
freeze_support()
test()
注意,在分布式多进程环境下,添加任务到Queue必须通过manager.get_task_queue()获得的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
27
28
29
30
31
32
33import 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() # 服务端用的是start()方法
# 获取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.')
正则表达式
正则表达式知识见笔者的另一篇笔记:https://www.zybuluo.com/king/note/43674
正则匹配默认是贪婪匹配。
Python提供re模块,包含所有正则表达式的功能。
1 | import re |
常用内建模块
datetime
datetime是Python处理日期和时间的标准库。
日期和时间部分的格式见:https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior1
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# datetime是模块,datetime模块还包含一个datetime类
from datetime import datetime
# 获取当前日期和时间
now = datetime.now() # 015-10-18 16:59:01.015529
# 用指定日期时间创建datetime
dt = datetime(2015, 4, 19, 12, 20)
# 1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time,记为0(1970年以前的时间timestamp为负数),当前时间就是相对于epoch time的秒数,称为timestamp,时间戳
# 全球各地的计算机在任意时刻的timestamp都是完全相同的
# datetime转换为timestamp
dt.timestamp() # 1429417200.0, 小数位表示毫秒数
# timestamp转换为datetime,本地时间
datetime.fromtimestamp(t)
# timestamp转换为datetime,UTC标准时区
datetime.utcfromtimestamp(t)
# str转换为datetime,转换后无时区信息
cday = datetime.strptime('2015-6-1 18:19:59', '%Y-%m-%d %H:%M:%S')
# datetime转换为str
dt.strftime('%a, %b %d %H:%M'))
# datetime加减,需要导入timedelta类
from datetime import datetime, timedelta
dt + timedelta(days=2, hours=12)
# 本地时间转换为UTC时间
# 一个datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个时区
from datetime import datetime, timedelta, timezone
# 创建时区UTC+8:00
tz_utc_8 = timezone(timedelta(hours=8))
now = datetime.now()
# 强制设置为UTC+8:00
dt = now.replace(tzinfo=tz_utc_8)
print(dt) # 2015-10-18 17:13:21.891555+08:00
# 时区转换
# 拿到UTC时间,并强制设置时区为UTC+0:00:
utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
# astimezone()将转换时区为北京时间:
bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
# astimezone()将转换时区为东京时间:
tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))
# astimezone()将bj_dt转换时区为东京时间:
tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9)))
collections
namedtuple
namedtuple是一个函数,它用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素。1
2
3
4
5
6
7
8
9
10
11from collections import namedtuple
# 定义一个点坐标
# namedtuple('名称', [属性list]):
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
# 引用元素
p.x
# 定义一个圆
Circle = namedtuple('Circle', ['x', 'y', 'r'])
deque
list是线性存储,数据量大的时候,插入和删除效率很低。
deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈。
1 | from collections import deque |
defaultdict
使用dict时,如果引用的Key不存在,就会抛出KeyError。如果希望key不存在时,返回一个默认值,就可以用defaultdict
1 | from collections import defaultdict |
OrderedDict
使用dict时,Key是无序的。在对dict做迭代时,我们无法确定Key的顺序。
如果要保持Key的顺序,可以用OrderedDict:
1 | from collections import OrderedDict |
OrderedDict可以实现一个FIFO(先进先出)的dict,当容量超出限制时,先删除最早添加的Key:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19from 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
Counter是一个简单的计数器
Counter实际上也是dict的一个子类1
2
3
4
5
6
7
8
9from collections import Counter
# 统计字符出现的个数
c = Counter()
for ch in 'programming':
c[ch] = c[ch] + 1
print(c)
# Counter({'g': 2, 'm': 2, 'r': 2, 'n': 1, 'i': 1, 'a': 1, 'p': 1, 'o': 1})
base64
Base64是一种用64个字符来表示任意二进制数据的方法。
Base64的原理很简单,首先,准备一个包含64个字符的数组:['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']
然后,对二进制数据进行处理,每3个字节一组,一共是3x8=24bit,划为4组,每组正好6个bit,可以表示0 ~ 2^6-1,一共64个int,对应上方的数组。查表获得相应的4个字符,就是编码后的字符串。
所以,Base64编码会把3字节的二进制数据编码为4字节的文本数据。如果要编码的二进制数据不是3的倍数,Base64用\x00字节在末尾补足后,再在编码的末尾加上1个或2个=号,表示补了多少字节,解码的时候,会自动去掉。
1 | import base64 |
struct
struct模块用于解决bytes和其他二进制数据类型的转换。
struct模块定义的数据类型可以参考Python官方文档:https://docs.python.org/3/library/struct.html#format-characters1
2
3
4
5
6
7
8
9import struct
# 把任意数据类型变成bytes
# pack的第一个参数是处理指令,>表示字节顺序是big-endian,也就是网络序,I表示4字节无符号整数。
# 后面的参数个数要和处理指令一致
struct.pack('>I', 10240099) # b'\x00\x9c@c'
# 把bytes变成相应的数据类型
# 根据>IH的说明,后面的bytes依次变为I:4字节无符号整数和H:2字节无符号整数
struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80') # (4042322160, 32896)
Windows的位图文件(.bmp)是一种非常简单的文件格式,我们来用struct分析一下。
首先找一个bmp文件,没有的话用“画图”画一个。读入前30个字节来分析:s = b'\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00'
BMP格式采用小端方式存储数据,文件头的结构按顺序如下:
两个字节:’BM’表示Windows位图,’BA’表示OS/2位图;
一个4字节整数:表示位图大小;
一个4字节整数:保留位,始终为0;
一个4字节整数:实际图像的偏移量;
一个4字节整数:Header的字节数;
一个4字节整数:图像宽度;
一个4字节整数:图像高度;
一个2字节整数:始终为1;
一个2字节整数:颜色数。
组合起来用unpack读取:struct.unpack('<ccIIIIIIHH', s)
结果为:(b'B', b'M', 691256, 0, 54, 40, 640, 360, 1, 24)
hashlib
Python的hashlib提供了常见的摘要算法,如MD5,SHA1等等。
摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。目的是为了发现原始数据是否被人篡改过。
摘要函数是一个单向函数,计算f(data)很容易,但通过digest反推data却非常困难。
MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,通常用一个32位的16进制字符串表示。
SHA1的结果是160 bit字节,通常用一个40位的16进制字符串表示。
比SHA1更安全的算法是SHA256和SHA512,不过越安全的算法不仅越慢,而且摘要长度更长。
有没有可能两个不同的数据通过某个摘要算法得到了相同的摘要?完全有可能,因为任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。这种情况称为碰撞,并非不可能出现,但是非常非常困难。
1 | import hashlib |
摘要算法应用如:存储用户口令的摘要。
为防止简单密码被黑客用MD5反推,对原始口令加一个复杂字符串再计算MD5,俗称“加盐”。经过Salt处理的MD5口令,只要Salt不被黑客知道,即使用户输入简单口令,也很难通过MD5反推明文口令。
要注意摘要算法不是加密算法,不能用于加密(因为无法通过摘要反推明文),只能用于防篡改,但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。
itertools
Python的内建模块itertools提供了非常有用的用于操作迭代对象的函数。
1 | import itertools |
XML
操作XML有两种方法:DOM和SAX。DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。SAX是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。
正常情况下,优先考虑SAX,因为DOM实在太占内存。
当SAX解析器读到一个节点时:<a href="/">python</a>
会产生3个事件:
start_element事件,在读取<a href="/">
时;
char_data事件,在读取python
时;
end_element事件,在读取</a>
时。
1 | 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) |
需要注意的是读取一大段字符串时,CharacterDataHandler可能被多次调用,所以需要自己保存起来,在EndElementHandler里面再合并。
生成XML:最简单也是最有效的生成XML的方法是拼接字符串。复杂的XML呢?建议你不要用XML,改成JSON。
HTMLParser
HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML1
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
33from 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 tutorial...<br>END</p>
</body></html>''')
urllib
Get
urllib的request模块可以发送一个GET请求到指定的页面,然后返回HTTP的响应:1
2
3
4
5
6
7
8from 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'))
如果我们要想模拟浏览器发送GET请求,就需要使用Request对象,通过往Request对象添加HTTP头,我们就可以把请求伪装成浏览器。例如,模拟iPhone 6去请求豆瓣首页:1
2
3
4
5
6
7
8
9from 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
如果要以POST发送一个请求,只需要把参数data以bytes形式传入。
我们模拟一个微博登录,先读取登录的邮箱和口令,然后按照weibo.cn的登录页的格式以username=xxx&password=xxx的编码传入:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25from 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'))
Handler
如果还需要更复杂的控制,比如通过一个Proxy去访问网站,我们需要利用ProxyHandler来处理,示例代码如下:1
2
3
4
5
6proxy_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
PIL:Python Imaging Library, 用于进行图像处理
由于PIL仅支持到Python 2.7,加上年久失修,于是一群志愿者在PIL的基础上创建了兼容的版本,名字叫Pillow,支持最新Python 3.x。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
55from PIL import Image
# ------------------缩小图像--------------------
# 打开一个jpg图像文件,注意是当前路径:
im = Image.open('test.jpg')
# 获得图像尺寸:
w, h = im.size
# 缩放到50%:
im.thumbnail((w//2, h//2))
# 把缩放后的图像用jpeg格式保存:
im.save('thumbnail.jpg', 'jpeg')
# -----------------模糊效果----------------------------------
# 应用模糊滤镜:
im2 = im.filter(ImageFilter.BLUR)
# ------------------生成字母验证码图片--------------------
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import random
# 随机字母:
def rndChar():
return chr(random.randint(65, 90))
# 随机颜色1:
def rndColor():
return (random.randint(64, 255), random.randint(64, 255), random.randint(64, 255))
# 随机颜色2:
def rndColor2():
return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127))
# 240 x 60:
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=rndColor())
# 输出文字:
for t in range(4):
draw.text((60 * t + 10, 10), rndChar(), font=font, fill=rndColor2())
# 模糊:
image = image.filter(ImageFilter.BLUR)
image.save('code.jpg', 'jpeg')
virtualenv
virtualenv就是用来为一个应用创建一套“隔离”的Python运行环境。需要用pip安装。
命令virtualenv就可以创建一个独立的Python运行环境,我们还加上了参数–no-site-packages,这样,已经安装到系统Python环境中的所有第三方包都不会复制过来。
此处教程写的是Mac,故笔记略过
图形界面
相关第三方库: Tk、wxWidgets、Qt、GTK
Python自带的tkinter封装了访问Tk的接口。
Tk支持多个操作系统,使用Tcl语言开发。Tk会调用操作系统的GUI接口。
tkinter
1 | from tkinter import * |
网络编程
TCP/IP简介
互联网协议簇(Internet Protocol Suite)就是通用协议标准。Internet是由inter和net两个单词组合起来的,原意就是连接“网络”的网络,有了Internet,任何私有网络,只要支持这个协议,就可以联入互联网。
互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议简称TCP/IP协议。
IP协议负责把数据从一台计算机通过网络发送到另一台计算。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。IPv6地址实际上是一个128位整数。
TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
一个IP包除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
TCP编程
Socket是网络编程的一个抽象概念。通常我们用一个Socket表示打开了一个网络链接,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
创建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# 导入socket库:
import socket
# 创建一个socket:
# AF_INET:IPv4 AF_INET6:IPv6
# SOCK_STREAM:面向流的TCP协议
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接。注意参数是tuple
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()
header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
# 把接收的数据写入文件:
with open(r'F:\sina.html', 'wb') as f:
f.write(html)
标准端口
Web:80
SMTP:25
FTP:21
端口号小于1024的是Internet标准服务的端口
端口号大于1024的,可以任意使用。
服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。
一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
服务器: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
28s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 监听端口。小于1024的端口号必须要有管理员权限才能绑定
s.bind(('127.0.0.1', 9999))
# 调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量:
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).encode('utf-8'))
sock.close()
print('Connection from %s:%s closed.' % addr)
客户端程序:1
2
3
4
5
6
7
8
9
10
11s = 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()
同一个端口,被一个Socket绑定了以后,就不能被别的Socket绑定了。
UDP
TCP建立可靠连接
UDP不需要建立连接,不保证到达
服务器:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import socket
# SOCK_DGRAM指定了这个Socket的类型是UDP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定端口:
s.bind(('127.0.0.1', 9999))
# 相比TCP,不需要调用listen()方法,直接接收来自任何客户端的数据
# 注意这里省掉了多线程
while True:
# 接收数据:
# recvfrom()方法返回数据和客户端的地址与端口
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
s.sendto(b'Hello, %s!' % data, addr)
客户端:1
2
3
4
5
6
7s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
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()
收发邮件
数据库
SQLite
SQLite是一种嵌入式数据库,它的数据库就是一个文件。C语言编写,体积很小。不能承受高并发访问。
操作关系数据库:
- 连接到数据库。一个数据库连接称为Connection。
- 打开游标,称之为Cursor。通过Cursor执行SQL语句,获得执行结果。
1 | # 导入SQLite驱动: |
查询上述数据库:1
2
3
4
5
6
7
8
9
10
11conn = sqlite3.connect('test.db')
cursor = conn.cursor()
# 执行查询语句:
cursor.execute('select * from user where id=?', '1')
# 获得查询结果集:
values = cursor.fetchall() # [('1', 'Michael')]
cursor.close()
conn.close()
打开后一定记得关闭。建议用try catch finally
使用Cursor对象执行insert,update,delete语句时,执行结果由rowcount返回影响的行数。
使用Cursor对象执行select语句时,通过featchall()可以拿到结果集。结果集是一个list,每个元素都是一个tuple,对应一行记录。
MySQL
MySQL内部有多种数据库引擎,最常用的引擎是支持数据库事务的InnoDB。
安装MySQL:http://dev.mysql.com/downloads/mysql/5.6.html
安装时请选择UTF-8编码。
安装MySQL驱动:pip install mysql-connector-python --allow-external mysql-connector-python
SQLAlchemy
数据库表是一个二维表,包含多行多列。一个list表示多行,list的每一个元素是tuple,表示一行记录。
Python的DB-API返回的数据结构这样表示:1
2
3
4
5[
('1', 'Michael'),
('2', 'Bob'),
('3', 'Adam')
]
用tuple表示一行很难看出表的结构。如果把一个tuple用class实例来表示,就可以更容易地看出表的结构来:1
2
3
4
5
6
7
8
9
10class 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,把关系数据库的表结构映射到对象上。
ORM框架用于做这个转换。Python中,最有名的ORM框架是SQLAlchemy
Web开发
响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误
HTTP GET请求的格式:1
2
3
4GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
每个Header一行一个,换行符是\r\n。
HTTP POST请求的格式:1
2
3
4
5
6POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
当遇到连续两个\r\n时,Header部分结束,后面的数据全部是Body。
HTTP响应的格式:1
2
3
4
5
6200 OK
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
HTTP响应如果包含body,也是通过\r\n\r\n来分隔的。请再次注意,Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。
当存在Content-Encoding时,Body数据是被压缩的
详细了解HTTP协议《HTTP权威指南》。
HTML定义了一套语法规则,告诉浏览器如何把一个丰富多彩的页面显示出来。
CSS是Cascading Style Sheets(层叠样式表)的简称,CSS用来控制HTML里的所有元素如何展现。
给标题元素<h1>
加一个样式,变成48号字体,灰色,带阴影:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<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>
JavaScript是为了让HTML具有交互性而作为脚本语言添加的,JavaScript既可以内嵌到HTML中,也可以从外部链接到HTML中。
当用户点击标题时把标题变成红色,就必须通过JavaScript来实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<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应用的本质就是:
- 浏览器发送一个HTTP请求;
- 服务器收到请求,生成一个HTML文档;
- 服务器把HTML文档作为HTTP响应的Body发送给浏览器;
- 浏览器收到HTTP响应,从HTTP Body取出HTML文档并显示。
WSGI:Web Server Gateway Interface,它只要求Web开发者实现一个函数,就可以响应HTTP请求。1
2
3
4
5
6
7
8
9# environ:一个包含所有HTTP请求信息的dict对象;
# start_response:一个发送HTTP响应的函数。
def application(environ, start_response):
# 发送HTTP响应的Header,只能发送一次
# 参数一是HTTP响应码
# 参数二是一组list表示的HTTP Header,每个Header用一个包含两个str的tuple表示
start_response('200 OK', [('Content-Type', 'text/html')])
# 发送HTTP响应的Body
return [b'<h1>Hello, web!</h1>']
application()函数由WSGI服务器来调用
Python内置了一个WSGI服务器,这个模块叫wsgiref,它是用纯Python编写的WSGI服务器的参考实现。所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。1
2
3
4
5
6
7
8
9
10
11
12
13# 从wsgiref模块导入:
from wsgiref.simple_server import make_server
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
body = '<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web')
return [body.encode('utf-8')]
# 创建一个服务器,IP地址为空,端口是8000,处理函数是application:
httpd = make_server('', 8000, application)
print('Serving HTTP on port 8000...')
# 开始监听HTTP请求:
httpd.serve_forever()
启动成功后,打开浏览器,输入http://localhost:8000/,就可以看到结果
使用Web框架 Flask
一个Web App,就是写一个WSGI的处理函数,针对每个HTTP请求进行响应。
在WSGI接口之上能进一步抽象,让我们专注于用一个函数处理一个URL,至于URL到函数的映射,就交给Web框架来做。
写一个app.py,处理3个URL,分别是:
- GET /:首页,返回Home;
- GET /signin:登录页,显示登录表单;
- POST /signin:处理登录表单,显示登录结果。
Flask通过Python的装饰器在内部自动地把URL和函数给关联起来。
1 | from flask import Flask |
除了Flask,常见的Python Web框架还有:
- Django:全能型Web框架;
- web.py:一个小巧的Web框架;
- Bottle:和Flask类似的Web框架;
- Tornado:Facebook的开源异步Web框架。
使用模板
使用模板,我们需要预先准备一个HTML文档,这个HTML文档不是普通的HTML,而是嵌入了一些变量和指令,然后,根据我们传入的数据,替换后,得到最终的HTML,发送给用户
MVC
- Python处理URL的函数就是C:Controller,Controller负责业务逻辑,比如检查用户名是否存在,取出用户信息等等;
- 包含变量的模板就是V:View,View负责显示逻辑,通过简单地替换一些变量,View最终输出的就是用户看到的HTML。
- Model是用来传给View的,这样View在替换变量的时候,就可以从Model中取出相应的数据。
Flask通过render_template()函数来实现模板的渲染。和Web框架类似,Python的模板也有很多种。Flask默认支持的模板是jinja2。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22from 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()
编写jinja2模板:
home.html
用来显示首页的模板:1
2
3
4
5
6
7
8<html>
<head>
<title>Home</title>
</head>
<body>
<h1 style="font-style:italic">Home</h1>
</body>
</html>
form.html
用来显示登录表单的模板:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<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</button></p>
</form>
</body>
</html>
signin-ok.html
登录成功的模板:1
2
3
4
5
6
7
8<html>
<head>
<title>Welcome, {{ username }}</title>
</head>
<body>
<p>Welcome, {{ username }}!</p>
</body>
</html>
一定要把模板放到正确的templates目录下,templates和app.py在同级目录下
Jinja2模板中,我们用表示一个需要替换的变量。很多时候,还需要循环、条件判断等指令语句,在Jinja2中,用{% ... %}
表示指令。
比如循环输出页码:1
2
3{% for i in page_list %}
<a href="/page/{{ i }}">{{ i }}</a>
{% endfor %}
除了Jinja2,常见的模板还有:
- Mako:用<% … %>和${xxx}的一个模板;
- Cheetah:也是用<% … %>和${xxx}的一个模板;
- Django:Django是一站式框架,内置一个用
{% ... %}
和的模板。
异步IO
同步IO:等待IO操作完成,才能继续进行下一步操作。
异步IO:当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:1
2
3
4loop = get_event_loop()
while True:
event = loop.get_event()
process_event(event)
协程
协程,又称微线程,纤程。英文名Coroutine。协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。
子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
和多线程比,最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程
Python对协程的支持是通过generator实现的。
在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。
协程实现的生产者-消费者模型:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# 一个generator
def consumer():
r = ''
while True:
# 当produce调用send语句时,这里的yield仅用来接收参数交赋值给n, consumer不会产生中断
# 当comsumer循环一圈后再执行到这里,此时produce还没有调用send,comsumer会中断执行
n = yield r # 拿到消息n,下面处理后再通过yield传回结果
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None) # 启动生成器,不会调用yield。参数不传None会报错,
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n) # 切换到consumer执行。拿到结果后继续
print('[PRODUCER] Consumer return: %s' % r)
c.close() # 关闭conumer
c = consumer()
produce(c)
asyncio
asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。
用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。
1 | import asyncio |
把asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了,因此可以实现并发执行。
用Task封装两个coroutine1
2
3
4
5
6
7
8
9
10
11
12
13import 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
5Hello world! (<_MainThread(MainThread, started 6132)>)
Hello world! (<_MainThread(MainThread, started 6132)>)
(----此处中断一秒----)
Hello again! (<_MainThread(MainThread, started 6132)>)
Hello again! (<_MainThread(MainThread, started 6132)>)
由打印的当前线程名称可以看出,两个coroutine是由同一个线程并发执行的。
如果把asyncio.sleep()换成真正的IO操作,则多个coroutine就可以由一个线程并发执行。
用asyncio的异步网络连接来获取sina、sohu和163的网站首页:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import asyncio
@asyncio.coroutine
def wget(host):
print('wget %s...' % host)
# 这一句只是创建asyncio协程,yield from connect 才是执行协程
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
15wget www.sohu.com...
wget www.sina.com.cn...
wget www.163.com...
(等待一段时间)
(打印出sohu的header)
www.sohu.com header > HTTP/1.1 200 OK
www.sohu.com header > Content-Type: text/html
...
(打印出sina的header)
www.sina.com.cn header > HTTP/1.1 200 OK
www.sina.com.cn header > Date: Wed, 20 May 2015 04:56:33 GMT
...
(打印出163的header)
www.163.com header > HTTP/1.0 302 Moved Temporarily
www.163.com header > Server: Cdn Cache Server V2.0
可见3个连接由一个线程通过coroutine并发完成。
async/await
用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。
为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。
async和await是针对coroutine的新语法,要使用新的语法,只需要做两步简单的替换:
- 把@asyncio.coroutine替换为async;
- 把yield from替换为await。
原代码:1
2
3
4
5@asyncio.coroutine
def hello():
print("Hello world!")
r = yield from asyncio.sleep(1)
print("Hello again!")
用新语法重新编写如下:1
2
3
4async def hello():
print("Hello world!")
r = await asyncio.sleep(1)
print("Hello again!")
aiohttp
把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。
编写一个HTTP服务器,分别处理以下URL:
/
- 首页返回b'<h1>Index</h1>'
;/hello/{name}
- 根据URL参数返回文本hello, %s!
。
1 | import asyncio |