From https://www.liaoxuefeng.com/
Python简介
Python是著名的“龟叔”Guido van Rossum在1989年圣诞节期间,为了打发无聊的圣诞节而编写的一个编程语言。
龟叔给Python的定位是“优雅”、“明确”、“简单”。
那Python适合开发哪些类型的应用呢?
首选是网络应用,包括网站、后台服务等等;
其次是许多日常需要的小工具,包括系统管理员需要的脚本任务等等;
另外就是把其他语言开发的程序再包装起来,方便使用。
Python的缺点:
1.慢;
2.代码不能加密。
安装Python
因为Python是跨平台的,它可以运行在Windows、Mac和各种Linux/Unix系统上。
https://wiki.python.org/moin/BeginnersGuide/Download
Python解释器
Python源文件是一个包含Python代码的以.py
为扩展名的文本文件。要运行代码,就需要Python解释器去执行.py
文件。
解释器 | Desc |
---|---|
CPython | 官方版本的解释器,以C语言开发。 |
IPython | 基于CPython的交互式解释器,执行Python代码的功能与CPython相同。CPython使用>>> 作为提示符,而IPython使用In [序号]: 作为提示符。 |
PyPy | PyPy对Python代码进行动态编译,所以可以显著提高Python代码的执行速度,PyPy于CPython不完全相同,相同的Python代码在两种解释器上可能产生不同的结果 PyPy与CPython的不同点 |
Jython | 运行在Java平台上的Python解释器 |
IronPython | 运行在.Net平台上的Python解释器 |
使用最广泛的还是CPython,如果要和Java或者.Net平台交互,最好的办法还是通过网络调用来交互,确保各程序间的独立性,而不是使用Jython或者IronPython。
第一个Python程序
Python交互式环境
1 | C:\Users\duxin>python |
1 | "hello world") print( |
OR
1 | #!/usr/bin/env python3 |
1 | D:\test\py>python hello_world.py |
文本编辑器
Sublime或者Notepad++
UTF-8 without BOM
4空格TAB 且 使用空格替换TAB
输入和输出
1 | # 逗号处会被替换成空格 |
1 | # hello.py |
1 | D:/test/py>python hello.py |
Python基础
以#
开头的语句是注释。
当语句以冒号:
结尾时,缩进的语句视为代码块。
大小写敏感。
数据类型和变量
整数:1,100,-8080,0xff00
Python对整数没有大小限制
浮点数:1.23,3.14,-9.01,1.23e9,12.3e8
浮点数运算可能会有四舍五入的误差。
Python对浮点数也没有大小限制,但超出一定范围就直接表示为inf
(infinity)。
字符串:’abc’,’xyz’,”I’m OK”,’I'm OK’
转义符:’\n’,’\t’,’\‘r''
表示''
内部的字符串默认不进行转义。
1 | desc = '''this is line1 |
''' '''
中可以包含多行内容,r''' '''
多行且不转义。
布尔值:True
,False
布尔运算:and
,or
,not
空值:None
变量:变量名必须是大小写英文,数字和_
组成,且不能用数字开头。
同一个变量可以反复赋值,且可以是不同类型的变量。
常量:在Python中,通常用全部大写的变量名表示常量PI = 3.14159265359
但事实上PI
仍然是一个变量,Python没有任何机制保证PI
不会被改变
浮点除:10 / 3
结果为3.3333333333333335
,9 / 3
结果为3.0
地板除:10 // 3
结果为3
取余:10 % 3
结果为1
Python支持多种数据类型,在计算机内部,可以把任何数据都看出一个“对象”,而变量就是在程序中用来指向这些数据对象的,对变量赋值就是把数据和变量给关联起来。
字符串和编码
字符 | ASCII | Unicode | UTF-8 |
---|---|---|---|
A | 01000001 | 00000000 01000001 | 01000001 |
中 | X | 01001110 00101101 | 11100100 10111000 10101101 |
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
编辑文件时,从文件中读取的UTF-8字符被转换为Unicode字符到内存中,编辑完成后,保存时再把Unicode转换为UTF-8保存到文件。
浏览网页时,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器。
Python字符串是以Unicode编码的。
1 | # 获取字符的整数表示 |
由于Python的字符串类型str
,在内存中以Unicode表示,一个字符对应若干个字节。
如果要在网络上传输,或者保存到磁盘上,就需要把str
变为以字节为单位的bytes
。
Python对bytes
类型的数据用带b
前缀的单引号或双引号表示:x = b'ABC'
。
以Unicode表示的str
通过encode()
方法可以编码为指定的bytes
:
1 | 'ABC'.encode('ascii') |
反过来,如果需要从网络或磁盘上读取了字节流,那么读到的数据就是bytes
,把bytes
变为str
,需要使用decode()
方法:
1 | b'ABC'.decode('ascii') |
Python中的len()
函数计算的是str
的字符数,如果换成bytes
,len()
计算的就是字节数。
1 | 'ABC') len( |
由于Python的源代码也是一个文本文件,当源代码中包含中文,在保存源代码时,就需要务必指明保存为UTF-8编码。
1 | #!/usr/bin/env python3 |
第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。
.py
源文件必须使用UTF-8 without BOM
编码保存。
在Python中,采用的格式化方式和C语言是一致的,用%
实现:
1 | 'Hello %s' % 'World' |
占位符 | Desc |
---|---|
%d |
整数 |
%f |
浮点数 |
%s |
字符串(永远起作用) |
%x |
十六进制整数 |
%% |
用%% 来表示一个% |
使用list和tuple
1 | # list是一个可变的有序表 |
1 | # tuple初始化后不能修改,不可变-有序 |
list和tuple是Python内置的有序集合,一个可变,一个不可变。
条件判断
1 | salary = 123 |
循环
1 | # 遍历 |
使用dict和set
dict
Python内置字典:
使用键-值(Key-Value)存储;
dict的Key必须是不可变对象(例如不可使用list用作Key);
dict内部存放数据的顺序与Key插入的顺序是无关的;
dict以空间换取时间。
当使用list时,随着list越长,查找和插入耗时越长;而dict查找和插入速度极快,且不会随着key的增加而变慢,但dict需要消耗更多的内存。
1 | 'Michael': 95, 'Bob': 75, 'Tracy': 85} d = { |
set
set是一组Key的集合,且Key不可重复。
1 | 1, 2, 3]) # 创建set s = set([ |
再议不可变对象
str是不变对象,而list是可变对象。
1 | 'c', 'b', 'a'] a = [ |
函数
在交互式命令行中可以通过调用help(FunctionName)
来查看函数帮助信息,例如help(abs)
。
调用函数
1 | 100) abs( |
1 | # 类型转换 |
1 | # alias |
定义函数
1 | def my_abs(x): |
Python使用def
语句定义函数,并依次写出函数名、括号、括号中的参数和冒号,然后在缩进块中编写函数体。
函数的返回值使用return
语句返回。
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为None
。return None
可以简写为return
。
当把my_abs()
函数定义保存为abstest.py
文件后,可以文件所在目录启动Python交互式命令行,并使用from abstest import my_abs
来导入my_abs()
函数。(注意:abstest
是文件名,不包含.py
扩展名)
如果想定义一个什么事也不做的空函数,可以使用pass
语句。
1 | def nop(): |
实际上pass
用来作为占位符,比如现在还没想好怎么写函数的代码,可以先放一个pass
,让代码可以运行起来,缺少了pass
,代码运行就会有语法错误。
调用函数时,如果参数的数量或者类型不正确,Python解释器会抛出TypeError
,可以使用isinstance()
函数进行类型检查。
1 | def my_abs(x): |
多值返回
1 | import math |
1 | # 多值返回实际上是返回一个tuple |
函数的参数
定义函数时,参数的名字和位置确定下来,函数的接口定义就完成了。对于函数调用者来说,只需知道如何传递正确的参数,以及函数将返回什么样的值就够了。
位置参数
1 | def power(x): |
1 | def power(x, n): |
默认参数
1 | def power(x, n = 2): |
必选参数在前,默认参数在后,否则Python解释器会报错。
当函数有多个参数时,把变化频繁的参数放在前面,变化不频繁的放在后面,变化不频繁的参数就可以作为默认参数。
1 | def add_end(L = []): |
Python函数在定义的时候,默认参数L
的值就被计算出来了,即[]
,因为默认参数L
也是一个变量,它指向对象[]
,每次调用该函数,如果改变了L
的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]
了。
默认参数必须指向不变对象。
1 | # 使用不变对象来实现 |
不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁。
在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
可变参数
1 | # calc 可接收任意个数的参数 |
关键字参数
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。
而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
1 | def person(name, age, **kw): |
**extra
表示把extra
这个dict的所有key-value用关键字参数传入到函数的**kw
参数,kw
将获得一个dict。
注意:kw
获得的dict是extra
的一份拷贝,对kw
的改动不会影响到函数外的extra
。
命名关键字参数
对于关键字函数,函数调用者可以传入任意不受限制的关键字参数。
命名关键字参数可以限制关键字参数的名字
1 | def person(name, age, *, city, job): |
和关键字参数**kw
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。
1 | 'Jack', 24, city = 'Beijing', job = 'Engineer') person( |
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了。
1 | def person(name, age, *args, city, job): |
命名关键字参数必须传入参数名。
命名关键字参数可以有缺省值。
1 | def person(name, age, *, city = 'Beijing', job): |
参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。
但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
比如定义一个函数,包含上述若干种参数:
1 | def f1(a, b, c = 0, *args, **kw): |
1 | 1, 2) f1( |
对于任意函数,都可以通过类似func(*args, **kw)
的形式调用它,无论它的参数是如何定义的。
小结
Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。
默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!
要注意定义可变参数和关键字参数的语法:
*args
是可变参数,args
接收的是一个tuple;
**kw
是关键字参数,kw
接收的是一个dict。
以及调用函数时如何传入可变参数和关键字参数的语法:
可变参数既可以直接传入:func(1, 2, 3)
,又可以先组装list或tuple,再通过*args
传入:func(*(1, 2, 3))
;
关键字参数既可以直接传入:func(a=1, b=2)
,又可以先组装dict,再通过**kw
传入:func(**{'a': 1, 'b': 2})
。
使用*args
和**kw
是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*
,否则定义的将是位置参数。
递归函数
1 | # n! |
Python并不支持尾递归优化,任何递归函数都存在栈溢出的问题。
高级特性
代码越少,开发效率越高。
切片
1 | 'Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] L = [ |
tuple和str同样可以切片操作
1 | 0, 1, 2, 3, 4, 5)[:3] ( |
迭代(Iteration)
在Python中,迭代是通过for ... in
来完成的。
默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values()
,如果要同时迭代key和value,可以用for k, v in d.items()
。
可通过collections模块的Iterable类型判断数据类型是否是可迭代对象。
1 | from collections import Iterable |
Python内置的enumerate
函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身。
1 | for i, value in enumerate(['A', 'B', 'C']): |
列表生成式
1 | 1, 11)) list(range( |
生成器
通过列表生成式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。
如果列表元素可以按照某种算法推算出来,那是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。
在Python中,这种一边循环一边计算的机制,称为生成器:generator。
1 | for x in range(10)] L = [x * x |
1 | for x in range(10)) g = (x * x |
使用函数实现列表生成器。
1 | # 打印-斐波拉契数列 |
1 | 6) f = fib( |
1 | a, b = b, a + b |
1 | def odd(): |
1 | o = odd() |
生成器在执行过程中,遇到yield
就会中断,下次调用又继续执行。
在for
循环过程中不断调用yield
,就会不断中断。
要给循环设置一个条件来退出循环,不然会产出一个无限数列出来。
使用for
循环调用generator时,发现拿不到generator的return
语句返回值。
如果要拿到返回值,必须捕获StopIteration
错误,返回值包含在StopIteration
的value
中:
1 | 6) g = fib( |
迭代器
可直接作用于for
循环的数据类型:
1.集合数据类型,如list
、tuple
、dict
、set
、str
等;
2.generator
,包括生成器和带yield
的generator function
。
这些可以直接作用于for
循环的对象统称为可迭代对象:Iterable
。
可以使用isinstance()
判断一个对象是否是Iterable
对象:
1 | from collections import Iterable |
生成器不但可以作用于for
循环,还可以被next()
函数不断调用并返回下一个值,直到最后抛出StopIteration
错误表示无法继续返回下一个值了。
可以被next()
函数调用并不断返回下一个值的对象称为:迭代器 Iterator
。
可以使用isinstance()
判断一个对象是否是Iterator
对象:
1 | from collections import Iterator |
生成器都是Iterator
对象。
list
、tuple
、dict
、set
、str
虽然是Iterable
,却不是Iterator
。
iter()
函数可以把Iterable
变成Iterator
:
1 | from collections import Iterator |
Python的Iterator
对象表示的是一个数据流,Iterator
对象可以被next()
函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration
错误。
可以把这个数据流看做是一个有序序列,却不能提前知道序列的长度,只能不断通过next()
函数实现按需计算下一个数据,所以Iterator
的计算是惰性的,只有在需要返回下一个数据时它才会计算。Iterator
甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
凡是可作用于for
循环的对象都是Iterable
类型;
凡是可作用于next()
函数的对象都是Iterator
类型,它们表示一个惰性计算的序列;
集合数据类型是Iterable
但不是Iterator
,不过可以通过iter()
函数获得一个Iterator
对象。
Python的for
循环本质上就是通过不断调用next()
函数实现的,例如:
1 | for x in [1, 2, 3, 4, 5]: |
函数式编程
通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
函数式编程(请注意多了一个“式”字)—— Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。
首先要搞明白计算机(Computer)和计算(Compute)的概念。
在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。
而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。
对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。
高阶函数
1 | # 函数也是变量 |
由于abs
函数实际上是定义在import builtins
模块中的,所以要让修改abs
变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10
。
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
1 | def add(x, y, f): |
map/reduce
Python内建了map()
和reduce()
函数。
Google论文MapReduce: Simplified Data Processing on Large Clusters
map
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
1 | def f(x): |
reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算。
1 | from functools import reduce |
1 | from functools import reduce |
filter
filter
把传入的函数依次作用于每个元素,然后根据返回值是(True
保留 | False
丢弃)该元素。
filter
函数返回的是一个Iterator
,也就是一个惰性序列,所以要强迫filter
完成计算结果,需要用list
函数获得所有结果并返回list
。
1 | def is_odd(n): |
用埃拉托斯特尼筛法计算素数。
首先,列出从2开始的所有自然数,构造一个序列:
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉:
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数5,然后用5把序列的5的倍数筛掉:
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
不断筛下去,就可以得到所有的素数。
1 | # 一个生成从3开始的奇数的生成器 |
sorted
Python内置的sorted
函数可以对list进行排序;
此外,sorted
函数也是一个高阶函数,它还可以接收一个key
函数来实现自定义的排序。
1 | 36, 5, -12, 9, -21]) sorted([ |
1 | L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)] |
返回函数
一个函数可以返回一个计算结果,也可以返回一个函数。
返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。
1 | def lazy_sum(*args): |
闭包
返回的函数在其定义内部引用了局部变量args
,所以当一个函数返回了一个函数后,其内部的局部变量还被新函数引用。
1 | def count(): |
错误观点认为:f1()
,f2()
,f3()
的结果应该为1
,4
,9
。
原因在于返回函数引用了变量i
,但它并非立刻执行。等到3个函数都返回时,i
已经变成了3
,因此结果为9
。
返回闭包时牢记一点:返回函数不要引用任何后续会发生变化的变量
。
以下方法可以“锁定”闭包中引用的变量(Like Erlang)。
1 | def count(): |
1 | f1, f2, f3 = count() |
可以利用 lambda函数 缩短代码:
1 | def count(): |
匿名函数
关键字lambda
表示匿名函数,冒号前的部分表示函数参数。
lambda
有个限制,就是只能有一个表达式,不用写return
,返回值就是该表达式的结果。
1 | lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])) list(map( |
装饰器
函数也是一个对象,而且函数对象还可以被赋值给变量,所以,通过变量也能调用该函数。
1 | def now(): |
假设现在需要增强now()
函数的功能,比如:在函数调用前后自动打印日志,但又不希望修改now()
函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
本质上,Decorator就是一个返回函数的高阶函数。
1 | def log(func): |
1 | now() |
log
是一个Decorator,返回一个函数。
原来的now
函数仍然存在,只是现在的now
变量指向了新的函数。
当调用now()
将执行新函数,即在log
函数中返回的wrapper
函数。
wrapper
函数的参数定义是(*args, **kw)
,因此,wrapper
函数可以接受任意参数的调用。
在wrapper
函数内,首先打印日志,再调用原始函数。
如果Decorator本身需要传入参数,那就需要编写一个返回Decorator的高阶函数。
比如,要自定义log
函数打印的文本:
1 | def log(text): |
1 | now() |
当执行now()
时,首先执行的是log('execute')
,返回值是一个dec
函数,再调用返回的dec
函数,参数是now
函数,返回值最终是wrapper
函数。
1 | now.__name__ |
经过Decorator装饰后,now
函数的__name__
属性已经从原来的'now'
变成了'wrapper'
。
now.__name__
的改变会导致有些依赖函数签名的代码执行出现错误。
Python内置的functools.wraps
可以修正这种情况。
1 | import functools |
1 | import functools |
通过import functools
导入functools
模块。
只需记住在定义wrapper
函数的前面加上@functools.wraps(func)
即可。
偏函数
Python的functools
模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。
1 | # int函数可以把字符串转换为整数 |
通过使用functools.partial
可以帮助创建一个偏函数,而不需要定义int2
函数
1 | import functools |
简单总结functools.partial
的作用就是:把一个函数的某些参数给固定住(设置默认值),返回一个新的函数。
创建偏函数时,实际上可以接收函数对象
、*args
、**kw
这3个参数。
1 | import functools |
1 | import functools |
模块
在Python中,一个.py
文件就称之为一个模块Module
。
按目录来组织模块的方法,称为包Package
。
一个abc.py
的文件就是一个名字叫abc
的模块,一个xyz.py
的文件就是一个名字叫xyz
的模块。
假设abc
和xyz
这两个模块名字与其他模块冲突了,可以通过包来组织模块,避免冲突。
方法是选择一个顶层包名,比如mypackage
,按照如下目录存放:
1 | mypackage/ |
引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。
现在,abc.py
模块的名字就变成了mypackage.abc
,类似的,xyz.py
的模块名变成了mypackage.xyz
。
注意:每一个包目录下面都会有一个__init__.py
的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py
可以是空文件,也可以有Python代码,因为__init__.py
本身就是一个模块,而它的模块名就是mypackage
。
类似的,可以有多级目录,组成多级层次的包结构。比如如下的目录结构:
1 | mypackage/ |
创建模块时要注意命名,不能和Python自带的模块名称冲突。例如,系统自带了sys
模块,自己的模块就不可命名为sys.py
,否则将无法导入系统自带的sys
模块。
使用模块
1 | #!/usr/bin/env python3 |
模块正常的函数和变量名是公开的Public
,可以被直接引用,如:abc
,x123
,PI
等;
类似__xx__
是特殊变量,可以被直接引用,但是有特殊用途,如:__name__
,__author__
,__doc__
等;
类似_xx
和__xx
是非公开的Private
,不应该
被直接引用,如:_abc
,__abc
等。
安装第三方模块
在Python中,安装第三方模块,是通过包管理工具pip
完成的。
https://pypi.python.org
当试图加载一个模块时,Python会在sys.path
指定的路径下搜索对应的.py
文件,找不到就会报错。
可使用修改 sys.path
和设置 PYTHONPATH 环境变量
两种方法添加搜索路径。
1 | # 修改 sys.path |
面向对象编程
Object Oriented Programming是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。
面向过程的程序设计把计算机程序视为一系列命令的集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切割成子函数,从而降低系统的复杂度。
而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收并处理其它对象发过来的消息。程序的执行就是一系列消息在各个对象之间传递。
在Python中,所数据类型都可以视为对象,也可以自定义对象。
自定义的对象数据类型就是面向对象中的类Class
的概念。
类Class
和实例Instance
1 | # class 关键字 |
1 | koko = Student() |
也可以在创建实例时,将必须的属性强制绑定上去:
1 | class Student(object): |
1 | 'ko', 60) ko = Student( |
类是创建实例的模板,实例是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;
通过在实例上调用方法,可直接操作了对象内部的数据,而无需知道方法内部的实现细节。
访问限制
如果要让类的内部属性不可以被外部访问,可以把属性的名称前加俩下划线__
,这就变成了一个私有变量,只有内部可以访问,外部不能访问。
1 | class Student(object): |
1 | 'bob', 70) bob = Student( |
虽然可以通过bob._Student__name
获取私有变量__name
,但强烈建议不要这么做,因为不同版本的Python解释器可能会把__name
改成不同的变量名。
1 | 'ski' bob.__name = |
变量命名 | Public/Private | 外部访问 | Desc |
---|---|---|---|
abc |
Public | Yes | 公有变量 |
_abc |
Private | Yes | 可以被外部访问,但应该被视为私有变量,不要随意访问。 |
__abc |
Private | No | 私有变量 |
__abc__ |
Public | Yes | 特殊变量,具有特殊意义。 |
继承和多态
在OOP中,每当定义一个class时,可以从某个现有的class中继承,新的class称为子类Subclass,而被继承的class称为基类、父类或者超类(Base class, Super class)。
1 | class Animal(object): |
1 | animal = Animal() |
当子类和父类都存在相同的run()
方法时,子类的run()
覆盖父类的run()
。这样就是继承的另一个好处:多态。
当定义一个class的时候,实际上就是定义了一种数据类型。这种自定义的类型和Python自带的数据类型(str
,list
,dict
)没什么两样。
1 | a = list() |
在继承关系中,如果一个实例的数据类型是某个子类,那么它的数据类型也可以被看做是父类。但是,反过来不行。
1 | def run_twice(animal): |
1 | run_twice(Animal()) |
1 | # 新增子类 |
1 | run_twice(Tortoise()) |
新增一个子类Tortoise
,而不必对run_twice()
做任何修改。
实际上,任何以Animal
做为参数的函数或者方法都可以不加修改的正常运行,原因就在于多态
。
对于一个变量,只需要知道它是Animal
类型,不需要知道确切的子类型,就可以放心地调用run()
方法。
而调用run()
方法时,执行的是Animal
或者Dog
或者Cat
或者Tortoise
中哪个run()
方法则由运行时该对象的确切类型决定。
开闭原则
:
对扩展开放(允许新增Animal
子类);
对修改封闭(不需要修改依赖Animal
类型的run_twice()
函数)。
继承可以一级一级地继承下来。任何类,最终都可以追溯到根类object
,继承关系像一棵树,object
是树根。
鸭子类型
对于静态语言(例如Java
),如果需要传入Animal
类型,则传入的对象必须是Animal
类型或者它的子类,否则将无法调用run()
方法;
而对于动态语言(例如Python
),则传入对象的不一定Animal
类型或者它的子类,只需要保证传入的对象有run()
方法就行了。
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那么它就可以被看做是鸭子。
Python的file-like object
就是一种鸭子类型。
对于真正的文件对象,具有一个read()
方法,返回其内容。
但是许多对象,只要含有read()
方法,都可以被视为file-like object
。
许多函数接收的参数就是file-like object
,不一定要传入真正的文件对象,只要实现了read()
方法的对象都可以作为这些函数的参数。
获取对象信息
使用type
函数判断对象类型
1 | 123) type( |
使用types
模块判断函数类型
1 | import types |
使用isinstance
判断变量类型
1 | d = Dog() |
使用dir
获得一个对象的所有属性和方法
1 | 'ABC') dir( |
类似__xxx__
的属性和方法在Python中都是有特殊用途的,比如__len__
返回长度。
在Python中,当调用len
获取长度时,实际上len
函数内部会自动去调用该对象是__len__
方法。
1 | 'ABC') len( |
自定义类可以通过写一个__len__
方法,实现len
函数
1 | class MyDog(object): |
通过getattr
, setattr
, hasattr
操作一个对象状态
1 | class MyObject(object): |
只有在不知道对象信息时,才需要去获取对象信息:
如果可以写成sum = obj.x + obj.y
,就不要写成sum = getattr(obj, 'x') + getattr(obj, 'y')
。
正确的使用范例:
1 | def readImage(fp): |
当从文件流fp
中读取图像,首先要判断该fp
对象是否存在read
方法。如果存在,则该对象是一个流;否则无法读取。
根据鸭子类型,有read
方法并不能代表该fp
对象就一定是一个文件流,也可以是网络流,或者内存中的一个字节流。但只要read
方法返回的是有效的图像数据,就不影响读取图像的功能。
实例属性和类属性
Python是动态语言,根据类创建的实例可以绑定任意属性。
1 | class Student(object): |
1 | 'puppy') s = Student( |
在开发过程中,千万不要将实例属性和类属性使用相同的名字,因为相同名字是实例属性将覆盖掉类属性,但当删除实例属性后,再使用相同的名字,访问到的将是类属性。
面向对象高级编程
当给一个自定义类创建实例之后,可以给该实例绑定任何属性和方法。
1 | class Student(object): |
动态绑定允许在程序运行过程中动态的给class添加功能,这在静态语言中很难实现。
__slots__
Python允许在定义类的时候,声明__slots__
特殊变量,从而限制绑定实例的属性名称。
1 | class Student(object): |
1 | s = Student() |
除非在子类中也定义__slots__
,这样子类实例允许定义的属性就是自身的__slots__
和父类的__slots__
的并集。
__slots__
只能限制实例直接添加属性,并不能限制实例通过添加方法来添加属性。
1 | class Stu(object): |
属性分实例属性和类属性,多个实例同时更改类属性,最后修改的值覆盖之前的值。
1 | class Stu(object): |
@property
1 | class Student(object): |
1 | s = Student() |
在对实例属性进行操作时,通过@property
,就可以不暴露原始属性,而是通过getter
和setter
方法来获取和设置属性值。
当只定义了getter
方法,而不定义setter
方法时,该属性实际上就是只读属性:
1 | class Student(object): |
多重继承
1 | class Animal(object): |
1 | class Dog(Mammal, Runnable): |
通过多重继承,一个子类可以同时获得多个父类的所有功能。
MixIn
为了更好地看出继承关系,把Runnable
和Flyable
改为RunnableMixIn
和FlyableMixIn
。
类似的,还可以定义出肉食动物CarnivorousMixIn
和植食动物HerbivoresMixIn
,让某个动物同时拥有好几个MixIn:
1 | class Dog(Mammal, RunnableMixIn, CarnivorousMixIn): |
在设计类的时候,优先考虑通过多重继承来组合多个MixIn
功能,而不是设计多层次的复杂的继承关系。
定制类
特殊命名 | Var/Method | Desc | ||
---|---|---|---|---|
__slots__ |
Var | Tuple格式,限制类属性 | ||
__len__ |
Method | 作用于len() 函数 |
||
__str__ |
Method | Format Print | ||
__repr__ |
Method | Format Debug Print | ||
__iter__ |
Method | 返回一个迭代对象 | ||
__next__ |
Method | 拿到循环下一个值,StopIteration 错误时退出循环 |
||
__getitem__ |
Method | 类似于list ordict ,按照下标取出元素,参数可以是切片对象 |
||
__setitem__ |
Method | 类似于list ordict ,为对象赋值 |
||
__delitem__ |
Method | 类似于list ordict ,删除某元素 |
||
__getattr__ |
Method | 默认情况下,当调用不存在的类属性会抛错,可通过添加该方法实现其它处理 | ||
__call__ |
Method | 直接调用实例本身,可通过callable 函数判断 |
||
#### 枚举类 | ||||
|
||||
|
||||
|
||||
#### 使用type 函数定义class |
||||
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。 |
1 | class Hello(object): |
1 | def fn(self, name='world'): |
使用type
函数创建class
需要依次传入三个参数:
1.class
的名称;
2.继承的父类集合,Python支持多重继承,如果只有一个父类,注意tuple
单元素写法;
3.class
的方法名称与函数绑定。
通过type
函数创建的类和直接定义的class
是完全一样的。
Python解释器遇到class
定义时,仅仅是扫描一下class
定义的语法,然后直接调用type
函数创建出class
。
metaclass
metaclass
,直译为元类
:
当定义了类之后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果想创建出类呢?那就必须根据metaclass
创建出类,所以:先定义metaclass
,然后创建类。
连接起来就是:先定义metaclass
,就可以创建类,最后创建实例。
按照约定,metaclass
的类名总是以Metaclass
结尾。
1 | # metaclass是类的模板,所以必须从 type 类型派生: |
当传入关键字参数metaclass
,Python解释器在创建MyList
时,会通过ListMetaclass.__new__
来创建。可以修改类的定义,比如增加新的方法,然后返回修改后的定义。
__new__
方法接收到的参数依次是:cls
当前准备创建的类的对象name
类的名字bases
类继承的父类集合attrs
类的方法集合
1 | # MyList可以调用add方法 |
ORM
ORM
全称”Object Relational Mapping”,即对象-关系映射
。
把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样写代码更简单,不用直接操作SQL语句。
要编写一个ORM
框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
如果使用者在使用一个ORM
框架,想定义一个User
类来操作对应的数据库表User
,只要写出这样的代码:
1 | class User(Model): |
其中,父类Model
和属性类型IntegerField
,StringField
都由ORM
框架提供。save
方法全部由metaclass
自动完成。
虽然metaclass
的编写会比较复杂,但ORM
的使用者调用会异常简单。
1 | # 定义Field类 负责保存数据库表的字段名和字段类型 |
当定义一个class User(Model)
时,Python解释器首先在当前类User
定义中查找metaclass
,如果没有找到,就继续在父类Model
中查找metaclass
,找到后就使用Model
中定义的metaclass
的ModelMetaclass
来创建User
类。
也就是说:metaclass
可以隐式地继承到子类,而子类本身却感觉不到。
在ModelMetaclass
中,实现了如下:
1.排除掉对Model
类的修改;
2.在当前类(比如User
)中查找定义的类的全部属性,如果找到Field
属性,就把它保存到一个__mappings__
的dict
中,同时从类属性中删除该Field
属性,否则会造成Run-Time Error
(实例的属性会遮盖类的同名属性);
3.把表名保存到__table__
中。
在Model
中,就可以定义各种操作数据库的方法,比如save
,delete
,find
,update
等等。
1 | 123, name='Bob', email='test@orm.org', passwd='rootoor') u = User(id= |
错误、调试和测试
错误处理
1 | # try 出错 |
1 | try... |
1 | # try 正常 |
1 | try... |
1 | # 多个 except 语句 |
1 | try... |
1 | # 没有错误抛出时 |
1 | try... |
Python的错误实际上也是class
,所有错误类型都是继承于BaseException
。
在使用except
时,需要注意的是它不但捕获该类型的错误,还会捕获该类型子类型的错误。
1 | try: |
错误类型的继承关系:
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
如果错误没有被捕获,它会沿着调用堆栈一直往上抛,直到最后被Python解释器捕获,打印出错误信息,然后程序退出。
如果不捕获错误,自然可以让Python解释器来打印出错误信息,但是程序也被终止了。
既然可以捕获错误,也就可以把错误信息打印出来的同时,让程序继续执行下去。
可以使用Python内置logging
模块用来记录错误消息。
1 | import logging |
1 | ERROR:root:division by zero |
同样的抛错,但程序在打印错误消息后还会继续执行(‘END’被打印),并且正常退出。
1 | # 自定义错误类型 |
1 | Traceback (most recent call last): |
只有在必要的时候才需要定义自己的错误类型。
如果可以选择Python内置的错误类型,就尽量使用内置的错误类型。
1 | def foo(s): |
1 | Catch ValueError! |
1 | try: |
1 | Traceback (most recent call last): |
assert
在调试程序的时候,可以在代码中插入print
函数,将变量打印出来。
还可以使用断言assert
来代替print
打印。
1 | def foo(s): |
1 | Traceback (most recent call last): |
如果断言失败,assert
语句本身就会抛出AssertionError
。
启动Python解释器时,可以使用-O
(大写字母O)参数来关闭assert
。
1 | $ python -O t_assert.py |
加入-O
参数后,所有assert
都可以当成pass
来看。
logging
和assert
相比,logging
不会抛出错误,并且可以输出到文件。
logging
可以指定记录信息的级别,有error
,warning
,info
,debug
等几个级别(日志等级从高到低):
当指定level=logging.INFO
时,比info
低的debug
(logging.debug
)就不起作用了;
同理当指定level=logging.WARNING
后,info
和debug
就不起作用了。
logging
还可以通过简单配置,使一条语句可以同时输出到不同的地方,比如Console和日志文件。
1 | import logging |
1 | INFO:root:n = 0 |
pdb Python Debugger
1 | # t_pdb.py |
启动 pdb
1 | $ python3 -m pdb t_pdb.py |
输入n
执行下一行;
输入p 变量名
查看变量值;
输入h
查看可使用命令;
输入q
退出。
1 | # t_pdb.py |
1 | $ python3 t_pdb.py |
程序运行到断点处会停止。
输入c
继续运行。
IDE
使用IDE可以比较方便的进行断点、单步调试。
PyCharm
https://www.jetbrains.com/pycharm/
单元测试unittest
TDDTest-Driven Development
测试驱动开发。
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
Python可以使用自带的unittest
模块来进行单元测试。
1 | # mydict.py |
1 | # mydict_test.py |
1 | # 运行方法一 |
1 | $ # 运行方法二 |
在测试模块中可以添加两个特殊的方法setUp
和tearDown
。
这两个方法会分别在调用每一个测试方法
的前、后分别被执行。
1 | import unittest |
将这两个方法添加到上述测试模块会产生如下效果:
1 | $ python3 -m unittest mydict_test |
1.单元测试可以有效地测试某个程序模块的行为;
2.单元测试的测试用例应该覆盖常用的输入组合、边界条件和异常;
3.单元测试代码要非常简单,不然无法保证测试代码本身不会有问题;
4.单元测试通过了并不意味着程序没有Bug,但是不通过一定有Bug。
文档测试doctest
Python内置的doctest
模块可以直接提取注释中的代码并执行测试。
1 | class Dict(dict): |
1 | $ python3 t_doctest.py |
doctest
严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。
只有测试异常
的时候,可以用...
表示中间一大段输出。
doctest
非常有用,不但可以用来测试,还可以直接作为示例代码。
通过某些文档生成工具,可以自动把包含doctest
的注释提取出来。
IO编程
IO
在计算机中指Input/Output
,也就是输入和输出。
由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。
IO
编程中,Stream
(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream
就是数据从外面(磁盘、网络)流进内存;Output Stream
就是数据从内存流到外面去。
由于CPU和内存的速度远远高于外设的速度,所以,在IO
编程中,就存在速度严重不匹配的问题。
举个例子来说,比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒。
有两种办法:
第一种是CPU等着,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO
;
另一种方法是CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以立刻接着执行,这种模式称为异步IO
。
同步
和异步
的区别就在于是否等待IO
执行的结果。
好比你去麦当劳点餐,你说“来个汉堡”,服务员告诉你,对不起,汉堡要现做,需要等5分钟,于是你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO
。
你说“来个汉堡”,服务员告诉你,汉堡需要等5分钟,你可以先去逛商场,等做好了,我们再通知你,这样你可以立刻去干别的事情(逛商场),这是异步IO
。
很明显,使用异步IO
来编写程序性能会远远高于同步IO
。
但是异步IO
的缺点是编程模型复杂。想想看,你得知道什么时候通知你“汉堡做好了”,而通知你的方法也各不相同。
如果是服务员跑过来找到你,这是回调模式
;
如果服务员发短信通知你,你就得不停地检查手机,这是轮询模式
。
操作IO
的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。
本章的IO
编程都是同步模式。
文件读写
在磁盘上读写文件的功能都是由操作系统提供的。
现代操作系统不允许普通的程序直接操作磁盘。
读写文件就是请求操作系统打开一个文件对象(文件描述符
),通过操作系统提供的接口从文件对象中读取数据或者把数据写入文件对象。
1 | try: |
由于调用read()
一次性读取文件的全部内容,如果文件很大有爆内存的风险,
保险起见,可以反复多次调用read(size)
方法,每次最多读取size
个字节的内容。
另外可以使用readline()
,每次读取一行内容。
或者readlines()
,一次读取所有内容并按行返回list
(读取配置文件常用)。
1 | for line in f.readlines(): |
类似于open
函数返回的具有read
方法的对象都是file-like Object
。
除了文件外,还可以是内存中的字节流、网络流、自定义流等等。file-like Object
不要求从特定的类继承,只要写个read
方法就行。
StringIO
就是在内存中创建的file-like Object
,常用作临时缓冲。
1 | # 二进制读取 |
StringIO
StringIO
实现了在内存中读写str
。
1 | from io import StringIO |
BytesIO
BytesIO
实现了在内存中读写bytes
。
1 | from io import BytesIO |
os
Python内置的os
模块可以直接调用操作系统提供的接口函数。
1 | import os |
pickle
Python提供的pickle
模块来实现序列化(Serialization)。
把变量从内存中变成可存储或传输的过程称之为序列化。
1 | import pickle |
json
JSON标准规定的JSON编码是UTF-8。
JSON表示的对象就是标准的JavaScript语言的对象。
JSON不仅是标准格式,并且比XML更快,而且可以直接在Web页面中读取。
JSON | Pyton |
---|---|
{} |
dict |
[] |
list |
"string" |
str |
123.456 |
int ,float |
true/false |
True/False |
null |
None |
Python内置的json
模块提供了Python对象到JSON格式的转换。
1 | import json |
1 | import json |
进程和线程
即使过去的单核CPU,也可以执行多任务。
由于CPU执行代码都是顺序执行的,操作系统可以轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。(时间片轮转调度法)
表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,用户感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现。
由于任务数量远远多于CPU的核心数量,操作系统也会自动把很多任务轮流调度到每个核心上执行。
对于操作系统来说,一个任务就是一个进程(Process
)。
在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,进程内的这些“子任务”称为线程(Thread
)。
由于每个进程至少要干一件事,所以,一个进程至少有一个线程。
多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。
当然,真正地同时执行多线程需要多核CPU才可能实现。
前面编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。
如果要同时执行多个任务:
1.启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。(多进程模式)
2.启动一个进程,在一个进程内启动多个线程,多个线程也可以一块执行多个任务。(多线程模式)
3.启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,这种模型更复杂,实际很少采用。(多进程+多线程模式)
Python既支持多进程,又支持多线程。
线程是最小的执行单元,而进程由至少一个线程组成。
如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享。
多进程multiprocessing
os.fork
Linux/Unix操作系统提供了一个fork
系统调用。
普通函数调用,调用一次,返回一次。
而fork
函数调用一次,返回两次,因为操作系统自动把当前进程(父进程)复制了一份(子进程),然后分别在父进程和子进程中返回。
子进程永远返回0
,而父进程则返回子进程的进程ID。
一个父进程可以fork
出很多子进程,所以,父进程要记录下每个子进程的进程ID。
而子进程只需要调用getppid
就可以获取父进程的进程ID。
Python的os
模块封装了常见的系统调用,其中就包括fork
:
1 | import os |
1 | $ python3 ./t_fork.py |
有了fork
调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork
出子进程来处理新的http请求。
multiprocessing.Process
由于Windows没有fork
调用,Python内置的multiprocessing
模块支持跨平台版本的多进程。multiprocessing
模块提供了一个Process
类来代表一个进程对象。
1 | from multiprocessing import Process |
1 | $ python3 t_multiprocessing.py |
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process
实例,用start
方法启动。join
方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
multiprocessing.Pool
如果要启动大量子进程,可以使用进程池的方式批量创建子进程:
1 | from multiprocessing import Pool |
1 | $ python3 ./t_multiprocessing.py |
对Pool
对象调用join
方法会等待所有子进程执行完毕。
在调用join
前必须调用close
方法,调用close
方法后就不能再添加新的Process
了。
subprocess
使用subprocess
模块启动一个子进程,然后控制其输入和输出。
1 | import subprocess |
1 | $ nslookup www.python.org |
1 | import subprocess |
1 | $ python3 ./t_subprocess.py |
multiprocessing.Queue
1 | from multiprocessing import Process, Queue |
1 | $ python3 ./t_multiprocessing.py |
进程间通信可以通过Queue
,Pipes
等实现。
由于Windows没有fork
调用,因此,multiprocessing
需要模拟出fork
的效果,父进程所有Python对象都必须通过pickle
序列化再传到子进程中去。
所以,如果multiprocessing
在Windows下调用失败了,首先要考虑是不是pickle
失败了。
多线程
Python标准库提供了两个模块:_thread
和threading
。_thread
是低级模块,threading
是高级模块,对_thread
进行了封装。
绝大多数情况下,只需要使用threading
这个高级模块。
1 | import time, threading |
1 | $ python3 t_thread.py |
由于任何进程都会默认启动一个子线程,该线程称为主线程。threading.current_thread()
会返回当前线程的实例。
主线程的名字叫MainThread
,子线程的名字在创建时指定,如果没有指定,会默认命名为Thread-1, Thread-2...
由于多线程会共享所属进程内的所有变量,所以在修改内容时要加锁。
1 | import threading |
1 | import threading |
如果存在多个锁,不同的线程持有不同的锁,并且试图获取对方持有的锁时,就可能造成“死锁”。
会导致多个线程全部阻塞,既不能执行,也无法结束,只能靠操作系统强制终止。
1 | import threading, multiprocessing |
Python的死循环代码并不能把CPU核心直接跑满:
由于Python的线程虽然是真正的线程,但是Python解释器在执行代码时,有一个GIL锁:(Global Interpreter Lock)(每个Python进程持有一个),任何Python线程执行前,必须先获取GIL锁,然而每执行100条字节码,Python解释器会自动释放GIL锁,让别的线程有机会执行。
GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程运行在100核CPU上,也只能用到1个核。
所以,在Python中,多线程并不能有效利用多核。
但是Python可以通过多进程实现多核任务。
多个Python进程有各自独立的GIL锁,互不影响。
ThreadLocal
在多线程的环境下,一个线程使用自己的局部变量比使用全局变量号。
因为局部变量只有线程自己能看见,修改不会影响其它线程,而全局变量的修改必须加锁。
1 | import threading |
1 | $ python3 t_ThreadLocal.py |
一个ThreadLocal
虽然是全局变量,但是每个线程只能读写自己线程的独立副本,互不干扰,也不用管理锁的问题,ThreadLocal
内部会处理。ThreadLocal
解决了参数在一个线程中各个函数之间相互传递的问题。ThreadLocal
常用的地方就是为每个线程绑定一个数据库连接、HTTP请求、用户身份信息等。
进程 VS 线程
要实现多任务,通常会设计Master-Worker
模式,Master
负责分配任务,Worker
负责执行任务,通常是一个Master
,多个Worker
。
如果用多进程实现Master-Worker
,主进程就是Master
,其他进程就是Worker
。
如果用多线程实现Master-Worker
,主线程就是Master
,其他线程就是Worker
。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)。
多进程模式的缺点是创建进程的代价大,在Unix/Linux
系统下,用fork
调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
由于切换进程/线程是有代价的,无论是多进程还是多线程,只要数量一多,效率肯定上不去。
是否需要采用多任务可通过任务类型判断,任务类型可以分为计算密集型
和IO密集型
:
计算密集型
任务的特点是要进行大量的计算,消耗CPU资源。(计算圆周率、对视频进行高清解码等等)。计算密集型
任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低。
所以,要最高效地利用CPU,同时进行的任务数量应当等于CPU的核心数。计算密集型
任务由于主要消耗CPU资源,因此,代码运行效率至关重要。
Python这样的脚本语言运行效率很低,完全不适合计算密集型
任务。对于计算密集型
任务,最好用C语言编写。
涉及到网络、磁盘IO的任务都是IO密集型
任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。
对于IO密集型
任务,任务越多,CPU效率越高,但也有一个限度。
常见的大部分任务都是IO密集型
任务,比如Web应用。IO密集型
任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少。
因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。
对于IO密集型
任务,最合适的语言就是开发效率最高的语言,脚本语言是首选,C语言最差。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO
。
如果充分利用操作系统提供的异步IO
支持,就可以用单进程单线程模型
来执行多任务,这种全新的模型称为事件驱动模型
。
Nginx就是支持异步IO
的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。
由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO
编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单线程的异步编程模型称为协程
,有了协程
的支持,就可以基于事件驱动
编写高效的多任务程序。
分布式进程
与threading
模块相比,multiprocessing
模块不仅仅支持通过多进程实现多任务,而且multiprocessing
中的managers
子模块还支持把多进程分布到多台机器上,而threading
最多只能分布到同一台机器的多个CPU上。
1 | # t_master.py |
1 | # t_worker.py |
在分布式多进程环境下,添加任务到Queue
不可以直接对原始的task_queue
进行操作,那样就绕过了QueueManager
的封装,必须通过manager.get_task_queue()
获得的Queue
接口添加。
Queue
之所以能通过网络访问,就是通过QueueManager
实现的。
由于QueueManager
管理的不止一个Queue
,所以,要给每个Queue
的网络调用接口起个名字,比如get_task_queue
。
authkey
是为了保证两台机器正常通信,不被其他机器恶意干扰。
Queue
的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小。
比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。
正则表达式
Python提供了re
模块,包含了所有正则表达式的功能。
1 | 'ABC\\-001' s = |
1 | import re |
1 | 'a b c d' s = |
1 | '021-123456' num = |
正则默认使用贪婪匹配
,也就是匹配尽可能多的字符。
1 | r'^([0-9]+)(0*)$', '102300').groups() re.match( |
如果一个正则表达式要重复使用几千次,出于效率的考虑,可以预编译该正则表达式:
1 | r'^([0-9]{3})-([0-9]{3,8})$') re_phone = re.compile( |
常用内建模块Batteries Included
datetime
Python内置datetime
处理日期和时间。
1 | from datetime import datetime |
1 | import datetime |
在计算机中,时间是用数字表示的。
把1970年1月1日 00:00:00 UTC+00:00时区
的时刻称为epoch time
,记为0
(1970年以前的时间timestamp
为负数),当前时间就是相对于epoch time
的秒数,称为timestamp
。
1 | timestamp = 0 = 1970-1-1 00:00:00 UTC+0:00 |
** timestamp
的值与时区毫无关系,因为timestamp
一旦确定,其UTC时间就确定了,转换到任意时区的时间也是完全确定的,这就是为什么计算机存储的当前时间是以timestamp
表示的,因为全球各地的计算机在任意时刻的timestamp
都是完全相同的(假定时间已校准)。 **
1 | from datetime import datetime |
** timestamp
没有时区的概念,而datetime
是有时区的。 **
1 | from datetime import datetime |
通过datetime.strptime
方法可以把字符串转换成datetime
。
1 | from datetime import datetime |
https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
通过datetime.strftime
方法可以把datetime
转换成字符串。
1 | from datetime import datetime |
对日期和时间进行加减实际上就是把datetime
往后或往前计算,得到新的datetime
。
加减可以直接使用+
和-
运算符,不过需要先导入timedelta
这个类。
1 | from datetime import datetime, timedelta |
本地时间是指系统设定时区的时间,例如北京时间是UTC+8:00
时区,而UTC时间是指UTC+0:00
时区的时间。
一个datetime
类型有一个时区属性tzinfo
,默认为None
,所以无法区分这个datetime
到底是哪个时区,除非强行给datetime
设置一个时区。
1 | from datetime import datetime, timedelta, timezone |
如果系统时区恰好是UTC+8:00
,那么上述代码就是正确的,否则,不能强制设置为UTF+8:00
时区的时间。
可以先通过utcnow()
拿到当前的UTC时间,再转换为任意时区的时间。
1 | print(datetime.now()) |
时区转换的关键在于:拿到一个datetime
时,要获知其正确的时区,然后强制设置时区,作为基准时间。
利用带时区的datetime
,通过astimezone
方法,可以转换到任意时区。
datetime
表示的时间需要时区信息才能确定一个特定的时间,否则只能视为本地时间。
如果要存储datetime
,最佳的方法是将其转换为timestamp
再存储,因为timestamp
的值与时区完全无关。
collections
函数namedtuple
可以用来创建一个自定义的tuple
对象,并且规定了tuple
元素的个数,并可以用属性而不是索引来引用tuple
的某个元素。
1 | 1, 2) point = ( |
使用list
存储数据时,按索引访问元素很快,但是插入和删除元素都很慢。
因为list
是线性存储,数据量大的时候,插入和删除效率很低。
deque
是为了高效实现插入和删除操作的双向列表,适合用于队列和栈。
1 | from collections import deque |
使用dict
时,如果引用的Key不存在,就会抛出KeyError
。
使用defaultdict
可以在访问不存在的Key时,返回一个默认值。
1 | from collections import defaultdict |
使用dict
时,Key是无序的。
在对dict
做迭代时,无法确定Key的顺序。
如果要保持Key的顺序,可以使用OrderedDict
。
1 | from collections import OrderedDict |
可以使用OrderedDict
实现一个FIFO
的dict
,当容量超出限制时,先删除最早添加的Key。
1 | # t_fifo_dict.py |
1 | from t_fifo_dict import * |
可以使用Counter
实现一个简单的字符统计,Counter
实际上就是一个dict
的子类。
1 | from collections import Counter |
base64
Base64是一种用64个字符来表示任意二进制数据的方法,是一种最常见的二进制编码方法。
Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%。
如果要编码的二进制数据不是3的倍数,Base64用\x00
字节在末尾补足后,再在编码的末尾加上1个或2个=
等号,表示补了多少字节,解码的时候,会自动去掉。
1 | import base64 |
由于标准的Base64编码后可能出现字符+
和/
,在URL中就不能直接作为参数,所以又有一种url safe
的Base64编码,会把字符+
和/
分别变成-
和_
。
1 | import base64 |
Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。
Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等。
由于=
字符也可能出现在Base64编码中,但=
用在URL、Cookie里面会造成歧义,所以,很多Base64编码后会把=去掉:
去掉=
后怎么解码呢?因为Base64是把3个字节变为4个字节,所以,Base64编码的长度永远是4的倍数,因此,需要加上=
把Base64字符串的长度变为4的倍数,就可以正常解码了。base64.b64decode(s + b'=' * (4 - len(s) % 4))
struct
Python没有专门处理字节的数据类型,b'str'
可表示字节。
Python提供struct
模块来处理bytes
和其它二进制数据类型的转换。
1 | import struct |
参数'>I'
中的>
表示字节顺序是big-endian,也就是网络序;I
表示4字节无符号整数。
参数'>IH'
中的H
表示2字节无符号整数。
https://docs.python.org/3/library/struct.html#format-characters
Python不适合编写底层操作字节流的代码。
hashlib
Python的hashlib
提供了常见的摘要算法,如MD5、SHA1等等。
摘要算法又称哈希算法、散列算法。它可以把任意长度的数据转换为一个长度固定的数据串(通常是16进制的字符串)。
摘要算法通过摘要函数f()
对任意长度的数据data
计算出固定长度的摘要digest
,目的是为了发现原始数据是否被篡改过。
摘要函数是一个单向函数,计算f(data)
很容易,而通过digest
反推data
却非常困难,而且对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。
1 | import hashlib |
MD5摘要算法速度很快,生成结果通常是一个32位的16进制字符串。
1 | import hashlib |
SHA1生成的结果通常是一个40位的16进制字符串。
比SHA1更安全的算法有SHA256和SHA512,但是摘要长度更长,生成速度也更慢。
** 任何摘要算法都是把无限多的数据集合映射到一个有限的集合中,有可能两个不同的数据确计算出相同的摘要,这种情况称为碰撞。 **
摘要算法通常用于在数据库中存储用户的密码:
一般不会直接在数据库中以明文的形式存储用户密码,而采用存储用户密码的摘要的方式,当用户登陆时,会先计算用户输入的明文密码的摘要,再和数据库存储的密码摘要对比,判断是否一致。
当黑客获取了数据库中存储的用户密码摘要时,不会采用费时费力的反推明文密码,而是会通过黑客事先计算的常用密码和摘要建立的反推表,来反推简单的密码,所以简单的密码很不安全。
可以通过“加盐”的方式使密码更加安全,在用户创建密码时,并不简单的存储密码的摘要,而是存储get_md5(passwd + 'The-Salt')
,这样会使简单密码的摘要也不可被黑客的反推表破解。
itertools
Python内建模块itertools
提供了用于操作迭代对象的函数。
1 | import itertools |
Python中itertools
模块提供的全部是处理迭代功能的函数,它们的返回值并不是list
,而是Iterator
,只有用for
循环迭代的时候才真正的计算惰性计算
。
contextlib
1 | try: |
并不只有open
函数返回的fp
对象才能使用with
语句。
只要实现了上下文管理
,就可以使用with
语句。
实现上下文管理
是通过__enter__
和__exit__
这两个方法:
1 | # t_with.py |
1 | from t_with import * |
通过实现__enter__
和__exit__
来使用with
语句依然很繁琐。
Python标准库提供的contextlib
有更简单的写法:
1 | # t_with.py |
1 | from t_with import * |
也可以用@contextmanager
实现在某段代码前后自动执行特定的代码:
1 | # t_with.py |
1 | from t_with import * |
代码执行的顺序是:
1.with
语句首先执行yield
之前的语句,因此打印出<h1>
2.yield
调用会执行with
语句内部的所有语句,因此打印出Hello
和World
3.最后执行yield
之后的语句,打印出</h1>
如果一个对象没有实现上下文管理
,就不能把它用于with
语句,这个时候,可用closing
方法把该对象变为上下文对象
:
1 | from contextlib import closing |
其实closing
也是一个经过@contextmanager
装饰的generator
,它的作用就是把任意对象
变为上下文对象
。
1 |
|
XML
XML虽然比JSON复杂,并且在Web中应用也不如以前多了,不过仍然有很多地方在用。
操作XML有两种方法:
1.DOM
会把整个XML读入内存,解析成树,因此占用内存大,解析慢,优点是可以任意遍历树的节点
2.SAX
是流模式,占用内存少,解析快,缺点是需要开发者自己处理事件
正常情况下,优先考虑使用SAX
。
Python中使用SAX
解析XML时,通常要关心的事件是start_element
,end_element
和char_data
。
当SAX
解析器读到一个XML节点时:<a href="/">python</a>
会产生3个事件,其中:
1.start_element
事件在读取<a href="/">
2.char_data
事件在读取python
3.end_element
事件在读取</a>
1 | # t_xml.py |
1 | $ python3 t_xml.py |
当注意的是读取一大段字符串时,CharacterDataHandler
可能被多次调用,所以需要先保存起来,然后在EndElementHandler
里面再合并。
** 如果需要生成复杂的XML时,考虑改用JSON。 **
HTMLParser
HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或者SAX来解析HTML。
1 | # t_html.py |
1 | $ python3 t_html.py |
其实并不需要一次性把整个HTML字符串都塞进去,feed
方法可以多次调用,这样可以一部分一部分塞进去。
HTML的特殊字符有两种表示方法:
1.命名实体,类似于
2.十进制编码,类似于 
这两种表示方法都可以通过Parser解析出来。
urllib
Python的urllib
提供了一系列用于操作URL的功能。
urllib
中的request
模块可以抓取URL中的内容,也就是发送一个GET
请求到指定的页面,然后返回HTTP响应。
1 | # t_urllib.py |
1 | $ python3 t_urllib.py |
通过往Request
对象添加HTTP头,可以把请求伪装成浏览器。
伪装的方法是先监控浏览器发出的请求,再根据浏览器的请求头来伪装,User-Agent头就是用来标识浏览器的。
例如,模拟iPhone6去请求豆瓣首页:
1 | from urllib import request |
如果要以POST
发送一个请求,只需要把参数data
以bytes
形式传入。
例如,模拟微博登陆:
1 | from urllib import request, parse |
如果需要通过Proxy
去访问网站,可以利用ProxyHandler
来处理。
1 | proxy_handler = urllib.request.ProxyHandler({'http': 'http://www.example.com:3128/'}) |
PIL(Python Imaging Library)
PIL已经是Python平台上图像处理标准库了。
由于PIL仅支持到Python2.7,志愿者在PIL的基础上创建了兼容的版本,名字叫Pillow,支持最新Python3.X。$ pip install pillow
生成字母验证码图片:
1 | from PIL import Image, ImageDraw, ImageFont, ImageFilter |
virtualenv
在开发Python应用的时候,当Python的版本是3.4时,所有第三方的包都会被pip
安装到Python3的site-packages目录下。
如果要同时开发多个应用,那这些应用都会共用一个Python。
如果应用A需要jinja 2.7,而应用B需要jinja 2.6怎么办?
这种情况下,每个应用可能需要各自拥有一套“独立”的Python运行环境。virtualenv
就是用来为每一个应用创建一套“隔离”的Python运行环境。pip install virtualenv
假定是要开发一个新的项目,需要一套独立的Python运行环境,可以这么做:
1 | $ # 1.创建目录 |
进入虚拟环境后,命令提示符会有个(venv)
前缀,表示当前环境是一个名为venv的Python环境。
当用命令source venv/bin/activate
进入一个virtualenv
环境时,会把系统Python复制一份到virtualenv
的环境,virtualenv
会修改相关环境变量,让命令python
和pip
均指向当前的virtualenv
环境。
图形界面
Python支持多种GUI编程的第三方库,包括:Tk、wxWidgets、Qt、GTK…
Python自带的库支持Tk的Tkinter,无需安装任何包,直接就可以使用。
Python代码会调用内置的Tkinter,Tkinter封装了访问Tk的接口;
Tk是一个图形库,使用Tcl语言开发,并且支持多个操作系统;
Tk会调用操作系统提供的本地GUI接口,完成最终的GUI。
1 | from tkinter import * |
在GUI中,每个Button、Label、输入框等,都是一个Widget。
Frame则是可以容纳其它Widget的Widget。
所有的Widget组合起来就是一棵树。
pack
方法把Widget加入到父容器中,并实现布局。pack
是最简单的布局,可以使用grid
实现更复杂的布局。
1 | from tkinter import * |
Python内置的Tkinter可以满足基本GUI程序的要求,如果要非常复杂的GUI程序,建议采用操作系统原生支持的语言和库来编写。
网络编程
计算机网络就是把各个计算机连接到一起,让网络中的计算机可以互相通信。
网络编程就是如何在程序中实现两台计算机的通信。
** 网络通信本质上是两台计算机上的两个进程之间的通信。 **
网络编程对所有开发语言都是一样的,Python也不例外。
用Python进行网络编程,就是在Python程序本身这个进程内,连接别的服务器进程的通信端口进行通信。
TCP/IP
为了把所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,互联网协议簇(Internet Protocol Suite)就是通用协议标准。
Internet是由”inter-net”两个单词组合起来的,原意就是“连接-网络”的网络,有了Internet,任何私有网络,只要支持这个协议,就可以联入互联网。
互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,把互联网的协议简称TCP/IP协议。
通信的时候,双方必须知道对方的标识。
互联网上每个计算机的唯一标识就是IP地址,类似123.123.123.123。
如果一台计算机同时接入到两个或更多的网络,比如路由器,它就会有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。
IP协议负责把数据从一台计算机通过网络发送到另一台计算机。
数据被分割成一小块一小块,然后通过IP包发送出去。
由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。
IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。
IPv4地址实际上是一个32位整数,以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。
IPv6地址实际上是一个128位整数,是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334。
TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。
TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
一个IP包除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
在两台计算机通信时,只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个IP包来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。
每个网络程序都向操作系统申请唯一的端口号,这样,两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。
一个进程也可能同时与多个计算机建立链接,因此它会申请很多端口。
TCP
Socket是网络编程中的一个抽象概念,通常用一个Socket表示“打开了一个网络链接”。
打开一个Socket需要知道目标计算机的IP地址和端口号,并且指定一个协议类型。
大多数连接都是可靠的TCP连接。
创建TCP时,由客户端主动发起连接,服务端被动响应。
1 | # Client |
服务端进程首先要绑定一个端口并监听来自其它客户端的连接。
由于服务端会有大量来自客户端的连接,所以,服务端要能够区分一个Socket连接是和哪个客户端绑定的。
一个Socket依赖4项来确定一个Socket:
1.Server IP
2.Server Port
3.Client IP
4.Client Port
由于服务端还要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理。
1 | # Server |
用TCP协议进行Socket编程在Python中十分简单:
对于客户端,要主动连接服务器的IP和指定端口,
对于服务端,要首先监听指定端口,对每一个新的连接,创建一个线程或进程来处理。
通常,服务端程序会无限运行下去。
UDP
TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。
UDP则是面向无连接的协议。
使用UDP协议时,不需要建立连接,只需要知道对方的IP和Port,就可以发送数据包,但是能不能到达就不知道了。
虽然UDP传输数据不可靠,但是比TCP速度快。
对于不要求可靠到达的数据,就可以使用UDP协议。
1 | # Server |
1 | # Client |
** 服务器绑定UDP端口和TCP端口互不冲突,也就是:UDP的9999端口和TCP的9999端口可以各自绑定。 **
电子邮件
电子邮件软件被称为MUAMail User Agent
邮件用户代理。
Email从MUA发出去,不是直接到达对方电脑,而是发到MTAMail Transfer Agent
邮件传输代理,就是那些Email服务提供商,比如网易、新浪等等。
假设使用网易的邮箱,发送给新浪的邮箱:
Email首先被投递到网易提供的MTA,再由网易的MTA发到对方服务商,也就是新浪的MTA。这个过程中间可能还会经过别的MTA,但是不用关心具体路线。
Email到达新浪的MTA后,新浪的MTA会把Email投递到邮件的最终目的地MDAMail Delivery Agent
邮件投递代理。
Email到达MDA后,就静静地躺在新浪的某个服务器上,存放在某个文件或特殊的数据库里,通常将这个长期保存邮件的地方称之为电子邮箱。
Email不会直接到达对方的电脑,因为对方电脑不一定开机,开机也不一定联网。对方要取到邮件,必须通过MUA从MDA上把邮件取到自己的电脑上。
所以,一封电子邮件的旅程就是:
** 发件人 -> MUA -> MTA -> 若干个MTA -> MTA -> MDA <- MUA <- 收件人 **
有了上述基本概念,要编写程序来发送和接收邮件,本质上就是:
1.编写MUA把邮件发到MTA;
2.编写MUA从MDA上收邮件。
发邮件时,MUA和MTA使用的协议就是SMTPSimple Mail Transfer Protocol
,MTA到另一个MTA也是用SMTP协议。
收邮件时,MUA和MDA使用的协议有两种:
1.POPPost Office Protocol
,目前版本是3,俗称POP3;
2.IMAPInternet Message Access Protocol
,目前版本是4,优点是不但能取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱移到垃圾箱,等等。
邮件客户端软件在发邮件时,先需要配置SMTP服务器,也就是要发到哪个MTA上。假设正在使用网易的邮箱,就不能直接发到新浪的MTA上,因为它只服务新浪的用户,所以,得填网易提供的SMTP服务器地址:smtp.163.com。
类似的,从MDA收邮件时,邮件客户端软件也会要求POP3或IMAP服务器地址,这样,MUA才能顺利地通过POP或IMAP协议从MDA取到邮件。
SMTP发送邮件
SMTP是发送邮件的协议。
Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件、带附件的邮件。
Python对SMTP的支持有smtplib
和email
两个模块,email
负责构造邮件,smtplib
负责发送邮件。
1 | # 1.构造纯文本邮件 |
1 | from email import encoders |
1 | # 发送HTML格式的邮件时 可以再添加一个纯文本邮件 |
1 | import smtplib |
https://docs.python.org/3/library/email.mime.html
POP3收取邮件
收取邮件就是编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或者手机上。
收取邮件最常用的协议是POP协议,目前版本号是3,俗称POP3。
Python内置一个poplib
模块,实现了POP3协议,可以直接用来收邮件。
POP3协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP协议很像,SMTP发送的也是经过编码后的一大段文本。
要把POP3收取的文本变成可以阅读的邮件,还需要用email
模块提供的各种类来解析原始文本,变成可阅读的邮件对象。
所以,收取邮件分两步:
1.用poplib把邮件的原始文本下载到本地;
2.用email解析原始文本,还原为邮件对象。
1 | # --- 通过POP3下载邮件 |
访问数据库
目前广泛使用的关系数据库也就这么几种:
00.付费的商用数据库:
01.Oracle,典型的高富帅;
02.SQL Server,微软自家产品,Windows定制专款;
03.DB2,IBM的产品,听起来挺高端;
04.Sybase,曾经跟微软是好基友,后来关系破裂,现在家境惨淡。
10.免费的开源数据库:
11.MySQL,大家都在用,一般错不了;
12.PostgreSQL,学术气息有点重,其实挺不错,但知名度没有MySQL高;
13.sqlite,嵌入式数据库,适合桌面和移动应用。
SQLite
SQLite是一种嵌入式数据库,它的数据库就是一个文件。
由于SQLite本身是C写的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在iOS和Android的App中都可以集成。
Python就内置了SQLite3,所以,在Python中使用SQLite,不需要安装任何东西,直接使用。
1 | # 导入SQLite驱动: |
使用Python的DB-API时,只要搞清楚Connection(连接)和Cursor(游标)对象,打开后一定记得关闭。
MySQL
MySQL是Web世界中使用最广泛的数据库服务器。
SQLite的特点是轻量级、可嵌入,但不能承受高并发访问,适合桌面和移动应用。
而MySQL是为服务器端设计的数据库,能承受高并发访问,同时占用的内存也远远大于SQLite。
MySQL内部有多种数据库引擎,最常用的引擎是支持数据库事务的InnoDB。
在Windows上,安装时请选择UTF-8编码,以便正确地处理中文。
在Mac或Linux上,需要编辑MySQL的配置文件,把数据库默认的编码全部改为UTF-8。
MySQL的配置文件默认存放在/etc/my.cnf或者/etc/mysql/my.cnf:
1 | [client] |
重启MySQL后生效。
** 如果MySQL的版本≥5.5.3,可以把编码设置为utf8mb4,utf8mb4和utf8完全兼容,但它支持最新的Unicode标准,可以显示emoji字符。 **
由于MySQL服务器以独立的进程运行,并通过网络对外服务,所以,需要支持Python的MySQL驱动来连接到MySQL服务器。
MySQL官方提供了mysql-connector-python
驱动,但是安装的时候需要给pip命令加上参数--allow-external
:
1 | $ pip install mysql-connector-python --allow-external mysql-connector-python |
1 | # 导入MySQL驱动: |
SQLAlchemy
数据库表是一个二维表,包含多行多列。
把一个表的内容用Python的数据结构表示出来的话,可以用一个list
表示多行,list
的每一个元素是tuple
,表示一行记录。
1 | # 包含id和name的user表: |
Python的DB-API返回的数据结构就是像上面这样表示的。
1 | # 如果把一个tuple用class实例来表示,就可以更容易地看出表的结构来: |
这就是ORM:Object-Relational Mapping,把关系数据库的表结构映射到对象上。
** 在Python中,最有名的ORM框架是SQLAlchemy。 **
1 | $ pip install sqlalchemy |
1 | from sqlalchemy import Column, String, create_engine |
1 | class User(Base): |
Web开发
随着PC的兴起,软件开始主要运行在桌面上,而数据库这样的软件运行在服务器端,这种Client/Server模式简称CS架构。
随着互联网的兴起,发现CS架构不适合Web,最大的原因是Web应用程序的修改和升级非常迅速,而CS架构需要每个客户端逐个升级桌面App,因此,Browser/Server模式开始流行,简称BS架构。
在BS架构下,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取Web页面,并把Web页面展示给用户即可。
Web开发经历了好几个阶段:
1.静态Web页面:由文本编辑器直接编辑并生成静态的HTML页面,如果要修改Web页面的内容,就需要再次编辑HTML源文件,早期的互联网Web页面就是静态的;
2.CGI:由于静态Web页面无法与用户交互,比如用户填写了一个注册表单,静态Web页面就无法处理。要处理用户发送的动态数据,出现了Common Gateway Interface,简称CGI,用C/C++编写。
3.ASP/JSP/PHP:由于Web应用特点是修改频繁,用C/C++这样的低级语言非常不适合Web开发,而脚本语言由于开发效率高,与HTML结合紧密,因此,迅速取代了CGI模式。ASP是微软推出的用VBScript脚本编程的Web开发技术,而JSP用Java来编写脚本,PHP本身则是开源的脚本语言。
4.MVC:为了解决直接用脚本语言嵌入HTML导致的可维护性差的问题,Web应用也引入了Model-View-Controller的模式,来简化Web开发。ASP发展为ASP.Net,JSP和PHP也有一大堆MVC框架。
HTTP
HTML是一种用来定义网页的文本,用来编写网页;
HTTP是在网络上传输HTML的协议,用于浏览器和服务器的通信。
HTTP请求的流程:
00.浏览器首先向服务器发送HTTP请求,请求包括:
01.方法:GET还是POST,GET仅请求资源,POST会附带用户数据(Body)
02.路径:/full/url/path
03.域名:由Host头指定:Host: www.sina.com.cn
04.以及其他相关的Header
10.服务器向浏览器返回HTTP响应,响应包括:
11.响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误
12.响应类型:由Content-Type指定
13.以及其他相关的Header
14.通常服务器的HTTP响应会携带内容,也就是有一个Body,包含响应的内容,网页的HTML源码就在Body中
20.如果浏览器还需要继续向服务器请求其他资源,比如图片,就再次发出HTTP请求,重复步骤00、10。
Web采用的HTTP协议采用了非常简单的请求-响应
模式,从而大大简化了开发。
当编写一个页面时,只需要在HTTP请求中把HTML发送出去,不需要考虑如何附带图片、视频等。
浏览器如果需要请求图片和视频,它会发送另一个HTTP请求,因此,一个HTTP请求只处理一个资源。
HTTP协议同时具备极强的扩展性,虽然浏览器请求的是Sina的首页,但是Sina在HTML中可以链入其他服务器的资源,
比如,<img src="http://xx.cn/home/2.png">
,从而将请求压力分散到各个服务器上,
而且,一个站点可以链接到其他站点,无数个站点互相链接起来,就形成了World Wide Web,简称WWW。
** 一个HTTP包含Header和Body两部分,其中Body是可选的。 **
1 | # HTTP GET |
1 | # HTTP POST |
1 | # HTTP 响应 |
Body的数据类型由Content-Type
头来确定:
1.如果是网页,Body就是文本
2.如果是图片,Body就是图片的二进制数据。
当存在Content-Encoding
时,Body数据是被压缩的,最常见的压缩方式是gzip,
当看到Content-Encoding: gzip
时,需要将Body数据先解压缩,才能得到真正的数据。
压缩的目的在于减少Body的大小,加快网络传输。
HTML
1 | <!-- HTML --> |
1 | <!-- CSS --> |
1 | <!-- JavaScript --> |
WSGI接口
Web应用的本质就是:
1.浏览器发送一个HTTP请求
2.服务器收到请求,生成一个HTML文档
3.服务器把HTML文档作为HTTP响应的Body发送给浏览器
4.浏览器收到HTTP响应,从HTTP的Body中取出HTML文档并显示
简单的Web应用就是先把HTML用文件保存好(静态):
使用一个现成的HTTP服务器软件,接收用户请求,从文件中读取HTML,返回。
常见的静态服务器软件:Apache、Nginx、Lighttpd等。
如果要动态生成HTML,就需要通过一个专门的服务器软件来实现:“接受HTTP请求”、“解析HTTP请求”、“发送HTTP响应”等苦力活,而使用Python专注于生成HTML文档就可以了。
Python和专门的服务器软件之间交互使用的统一的接口就是WSGI:Web Server Gateway Interface。
1 | # hello.py |
上面的application
函数就是一个符合WSGI标准的HTTP处理函数:
1.参数environ
是一个包含所有HTTP请求信息的dict
2.参数start_response
是一个发送HTTP响应的函数(只能调用一次)
3.返回值将作为HTTP响应的Body发送给浏览器
4.函数application
必须由WSGI服务器调用
有了WSGI,编写代码时只需要关心:
1.从environ
这个dict
中取出HTTP请求信息
2.构造HTML
3.通过start_response
发送Header
4.返回Body
Python内置一个WSGI服务器wsgiref
,它是用纯Python编写的WSGI服务器“参考实现”。
** 所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何效率,仅供开发和测试使用。 **
1 | # server.py |
1 | # hello.py |
Web框架
常见的Python Web框架有:
1.Flask:小巧、灵活
2.Django:全能型Web框架
3.web.py:一个小巧的Web框架
4.Bottle:和Flask类似的Web框架
5.Tornado:Facebook的开源异步Web框架
有了Web框架,在编写Web应用时,注意力就从“WSGI处理函数”转移到“URL+对应的处理函数”,这样,编写Web应用就更加简单了。
在编写URL处理函数时,除了配置URL外,从HTTP请求拿到用户数据也是非常重要的。
Web框架都提供了自己的API来实现这些功能。
Flask通过request.form['name']
来获取表单的内容。
1 | $ pip install flask |
1 | from flask import Flask |
模板
MVC:Model-View-Controller 模型-视图-控制器
Python处理URL的函数就是C:Controller,Controller负责业务逻辑,比如检查用户名是否存在,取出用户信息等;
包含变量{{ name }}
的模板就是V:View,View负责显示逻辑,通过简单地替换一些变量,View最终输出就是用户看到的HTML;
Model是用来传给View的,View在替换变量的时候,是从Model中取出相应的数据,上例中,Model就是一个dict
:{'name': 'Michael'}
,由于Python支持关键字参数,很多Web框架允许传入关键字参数,然后在框架内部组装出一个dict
作为Model。
Flask通过render_template
函数来实现模板的渲染。
和Web框架类似,Python的模板也有很多种。
Flask默认支持的模板是jinja2
1 | $ pip install jinja2 |
1 | $ tree . |
1 | # app.py |
1 | <!-- home.html --> |
1 | <!-- form.html --> |
1 | <!-- signin-ok.html --> |
只需要在Python代码中处理M:Model和C:Controller,而V:View是通过模板处理的,这样就把Python代码和HTML代码最大限度地分离了。
使用模板的另外一个好处是,模板改起来很方便,而且,改完保存后,刷新浏览器就能看到最新的效果。
在Jinja2中,如果需要循环、条件判断等指令语句,可以用{% ... %}
表示指令。
1 | <!-- 循环输出页码 --> |
异步IO
CPU的速度远远快于磁盘、网络等IO。
在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。
这种情况称为同步IO
。
在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。
因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以必须使用多线程或者多进程来并发执行代码,为多个用户服务。
每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。
多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。
由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。
由于要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的方法之一。
另一种解决IO问题的方法是异步IO
。
当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。
一段时间后,当IO返回结果时,再通知CPU进行处理。
可以想象如果按普通顺序写出的代码实际上是没法完成异步IO的:
1 | do_some_code() |
异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:
1 | loop = get_event_loop() |
消息模型其实早在应用在桌面应用程序中了。
一个GUI程序的主线程就负责不停地读取消息并处理消息。
所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后由GUI程序的主线程处理。
由于GUI线程处理键盘、鼠标等消息的速度非常快,所以用户感觉不到延迟。
某些时候,GUI线程在一个消息处理的过程中遇到问题导致一次消息处理时间过长,此时,用户会感觉到整个GUI程序停止响应了,敲键盘、点鼠标都没有反应。
这种情况说明在消息模型中,处理一个消息必须非常迅速,否则,主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。
当遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。
当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。
在“发出IO请求”到收到“IO完成”的这段时间里,同步IO
模型下,主线程只能挂起,但异步IO
模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。
这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。
对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。
协程
协程,又称微线程,纤程。英文名Coroutine
。
协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
** 子程序调用是通过栈实现的,一个线程就是执行一个子程序。 **
** 子程序调用总是一个入口,一次返回,调用顺序是明确的。 **
协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。
比如子程序A、B:
1 | def A(): |
假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能
是:
1 | 1 |
看起来A、B的执行有点像多线程。
** 协程的特点在于是一个线程执行。 **
和多线程比,协程有优势:
1.协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
2.不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
** “多进程+协程”:既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。 **
Python对协程的支持是通过generator
实现的。
在generator
中,不仅可以通过for
循环来迭代,还可以不断调用next
函数获取由yield
语句返回的下一个值。
但是Python的yield
不仅可以返回一个值,还可以接收调用者发出的参数。
来看例子:
传统的“生产者-消费者模型”是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield
跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
1 | def consumer(): |
1 | [PRODUCER] Producing 1... |
注意到consumer
函数是一个generator
,把一个consumer
传入produce
后:
1.调用c.send(None)
启动生成器
2.通过c.send(n)
切换到consumer
执行
3.consumer
通过yield
拿到消息,处理,又通过yield
把结果传回
4.produce
拿到consumer
处理的结果,继续生产下一条消息
5.produce
决定不生产了,通过c.close()
关闭consumer
,整个过程结束
整个流程无锁,由一个线程执行,produce
和consumer
协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
最后套用Donald Knuth的一句话总结协程的特点:
** “子程序就是协程的一种特例。” **
asyncio
asyncio
是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio
的编程模型就是一个消息循环。
从asyncio
模块中直接获取一个EventLoop
的引用,然后把需要执行的协程扔到EventLoop
中执行,就实现了异步IO。
1 | import asyncio |
装饰器@asyncio.coroutine
把一个generator
标记为coroutine
类型,然后,就可以把这个coroutine
扔到EventLoop
中执行。
hello()
会首先打印出Hello World!,然后,yield from
语法可以调用另一个generator
。
由于asyncio.sleep()
也是一个coroutine
,所以线程不会等待asyncio.sleep()
,而是直接中断并执行下一个消息循环。
当asyncio.sleep()
返回时,线程就可以从yield from
拿到返回值(此处是None
),然后接着执行下一行语句。
把asyncio.sleep(1)
看成是一个耗时1秒的IO操作。
在此期间,主线程并未等待,而是去执行EventLoop
中其他可以执行的coroutine
了。
因此可以用“一个线程实现并发执行”。
1 | import threading |
1 | Hello World! (<_MainThread(MainThread, started 140426655303488)>) |
根据打印的当前线程名称可以看出,两个coroutine
是由同一个线程并发执行的。
1 | import asyncio |
1 | wget www.sina.com.cn... |
async
/await
Python从3.5版本开始为asyncio
提供了async
和await
的新语法。
async
和await
是针对coroutine
的新语法,可以让coroutine
的代码更简洁易读。
要使用新的语法,只需要做两步简单的替换:
1.把@asyncio.coroutine
替换为async
2.把yield from
替换为await
1 | import asyncio |
用新语法重新编写如下:
1 | import asyncio |
aiohttp
asyncio
可以实现单线程并发IO操作。
如果仅用在客户端,发挥的威力不大。
如果把asyncio
用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用“单线程+coroutine
”实现多用户的高并发支持。asyncio
实现了TCP、UDP、SSL等协议。
aiohttp
则是基于asyncio
实现的HTTP框架。
1 | $ pip install aiohttp |
1 | import asyncio |