python笔记

1.Unicode把所有语言都统一到一套编码中,因此不会有乱码问题。
2.若写的文本基本上全部都是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。
3.ASCIi编码实际上可以被看成是UTF-8编码的一部分,所以,大量只支持ASCII编码的历史遗留软件可以在UTF-8编码下继续工作。
4.在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输时,就转换为UTF-8编码;
用记事本编辑的时候,从文本读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件;
浏览网页时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器。
5.由于Python的字符串类型是str,在内存中以Unicode表示,一个字节对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str变成以字节为单位的bytes。
以Unicode表示的str通过encode()方法可以编码为指定的bytes,’ABC’.encode(‘acsii’),b’ABC’,’中文’.encode(‘utf-8’),b’xe4\xb8\xad\xe6\x96\x87’(含有中文的str无法用ASCII编码,因为中文编码的范围超过了ASCII编码的范围。
在bytes中,无法显示为ASCII字符的字节,用\x##显示。
反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变成str,就需要用decode()方法: b’ABC’.decode(‘ascii’)。
如果bytes中只有一小部分无效的字节,可以传入errors=’ignore’忽略错误的字节:
b’\xe4\xb8\xad\xff’.decode(‘utf-8’,errors = ‘ignore’) ‘中’
计算str字符数,len(‘ABC’),3,len(‘中文’),2.
换成bytea,len()函数就计算字节数,len(b’ABC’),3,len(‘中文’.encode(‘utf-8’)),6
6.Python内置的一种数据类型是列表:list。A = [1,’sas’],list是一种有序的集合,可以随时添加和删除其中的元素。追加元素到末尾,.append();把元素插入到指定的位置,比如索引号为1的位置,.insert(1,’jack’);删除list末尾的元素,.pop();把某个元素替换成别的元素,直接赋值给对应的索引位置。
7.另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,没有append(),pop()这样的方法。
只有一个元素的tuple定义时必须加一个逗号,,来消除歧义。T = (1,)
“可变的”tuple T = (‘a’,’b’,[‘A’,’B’]),里面list的元素可变。tuple所谓的“不变”是说,tuple的每一个元素,指向永远不变。
8.python中内置了字典:dict的支持,dict全称dictionary,在其他语言中也被称为map,使用健-值(key-value)存储,具有极快的查找速度。D = {‘mike’: 95,’nike’: 75,’Nacy’: 88}
D[‘mike’],95
避免key不存在的两种方法, ‘Tomas’ in D,false;D.get(‘Tomas’),None D.get(‘Tomas’,-1),-1。
要删除一个key,用pop(key)方法,对应的value也会从dict种删除。
dict:
(1) 查找和插入的速度极快,不会随着key的增加而变慢;
(2) 需要占用大量的内存,内存浪费多
list:
(1) 查找和插入的时间随着元素的增加而增加;
(2) 占用空间小,浪费内存很少。
*dict的key必须是不可变对象。因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那么dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。
9.set和dict类似,也是一组key的组合,但不存储value。由于key不能重复,所以在set种,没有重复的key。S = set([1,2,3])。
10. a = ‘abc’ b = a.replace(‘a’,’A’) a,’abc’ b,’Abc’ a是变量,而’abc才是字符串对象。有时候我们常说,对象a的内容是’abc’,但其实是指,a本身是一个变量,它指向的对象的内容才是’abc’。
对于不变对象来说,调用对象本身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。
11.在Python中,定义一个函数要使用def语句,依次写出函数名,括号,括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。
若已经把my_hanshu()的函数定义保存为mytest.py文件了,那么,可以在该文件的当前目录下启动Python解释器,用from mytest import my_hanshu来导入my_hanshu()函数,注意mytest是文件名。
如果想定义一个什么事也不做的空函数,可以用pass语句。实际上pass可以用来作为占位符,比如现在还没有想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。
数据类型检查可以用内置函数isinstance()实现:if not isinstance(x,(int,float))
12.默认函数可以简化函数的调用,降低调用函数的难度。设置默认参数时,注意:(1)必选参数在前,默认参数在后,否则Python的解释器会报错;(2)如何设置默认参数 当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。
默认参数必须指向不变对象 def end(L =None)
为什么要设计str、None这样的不变对象?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。编写程序时,如果可以设计一个不变对象,就
尽量设计成不变对象。
13.在Python函数中,还可以定义可变参数。把函数的参数改为可变参数。定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个
号。可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum

calc(1,2)
14.关键字参数允许传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。关键字参数可以扩展函数的功能。
def person(name,age,**kw)
printf(‘name:’,name,’age:’,age,’others:’,kw)

extra = {‘city’ : ‘Beijing’,’job’ : ‘Engineer’}
person(‘Jack’,24,city = extra[‘city’], job = extra[‘job’])(person(‘Jack’,24,**extra),extra表示把extra这个dict的所有key-value用关键字参数传入到函数的kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra)
15.取指定索引范围的元素,Python提供了切片(Slice)操作符。
L[0:3] 从索引0开始取,取索引0,1,2对应元素
L[-2:0] 取倒数两个元素
L[:10:2] 前十个数,从索引0对应元素开始,每两个取一个
L[::5] 所有数,从索引0对应元素开始,每5个取1个
16.列表生成式
list(range(1,11)) [1,2,3,4,5,6,7,8,9,10]
[xx for x in range(1,11)] [1,4,9,16,25,36,49,64,81,100]
[x
x for x in range(1,11) if x%2 == 0] [4,16,36,64,100]
[m+n dor m in ’AB’ for n in ‘XY’] [‘AX’,’AY’,’BX’,’BY’]
17.函数本身也可以赋值给变量,即变量可以指向函数。
f = abs
把函数作为参数传入,这样的函数称为高阶函数。函数式编程就是指这种高度抽象的编程范式。
def add(x,y,f):
return f(x) + f(y)
18.Python中,一种一边循环,一边计算的机制,称为生成器:generator。创建一个generator,第一种方法,把一个列表生成式的[]改成(),就创建了一个generator
L = (xx for x in range(10))
第二种方法,如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator
函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()语句的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
19.可以直接作用于for循环的对象统称为可迭代对象:Iterable
一类是集合数据类型,如list,tuple,dict,set,str等
一类是generator,包括生成器和带yield的generator function
可以使用isinstance()判断一个对象是否是Iterable对象
Isinstance(‘abc’,Iterable) True
20.Python内建了map()和reduce()函数
map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。
def f(x):
return x
x
r = map(f,[1,2,3,4])
list®
[1,4,9,16]
reduce把一个函数作用在一个序列[x1,x2,…]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:reduce(f,[x1,x2,x3,x4]) = f(f(f(x1,x2),x3),x4)
21.Python内建的filter()函数用于过滤序列。
filter()也接收一个函数和一个序列,把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
def is_odd(n):
return n % 2 == 1
list(filter(is_odd,[1,2,4,5,7])

def not_empty(s)
return s and s.strip()
list(filter(not_empty,[‘a’,’’,’b’,None,’c’,’ ‘])) [‘a’,’b’,’c’]
22. Python内置的sorted()函数可以对list进行排序,此外,它也是一个高阶函数,还可以接收一个key函数来实现自定义的排序
sorted([36,5,-12,9,-21],key = abs)
key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。
23.函数作为返回值。当不需要立刻求和,而是在后面的代码中,根据需要再计算。可以不返回求和的结果,而是返回求和的函数:
def lazy_sum(args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
当调用lazy_sum()时,返回的不是求和结果,而是求和函数: f = lazy_sum(1,3,4,5)
调用函数f时,才是真正计算求和的结果: f()
内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lay_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包”的程序结构。
24.在传入函数的时候,有时候不需要显式地定义函数,直接传入匿名函数更方便。
list(map(lambba x: x
x,[1,2,3,4,5,6,7,8,9]
匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数
f = lambba x: xx
f(5)
同样,也可以把匿名函数作为返回值返回
def build(x,y):
return lambba: x
x+y*y
25.装饰器。在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。本质上,decorator就是一个返回函数的高阶函数。
定义一个能打印日志的decorator,可定义如下:
def log(func):
def wrapper(*args,**kw):
print(‘call %s():’ %func.name)
return func(*args,**kw)
return wrapper
需借助Python的@语法,把decorator置于函数的定义处:

@log
def now():
print(‘2019-01-19’)
调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:
now() call now(): 2019-01-19
把@log放在now()函数的定义处,相当于执行了语句:
now = log(now)
由于log()是一个decorator,返回一个函数,所以,原来的now()仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。wrapper()函数的参数定义是(*args,**kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。
如果decorator本身需要传入参数,则需要编写一个返回decorator的高阶函数,如
def log(text):
def decorator(func):
def wrapper(*args,**kw):
print(‘%s %s():’ % (text,func.name))
return func(*args,**kw)
return wrapper
return decorator
该3层嵌套的decorator用法如下:
@log(‘execute’)
def now():
print(‘2019-01-19’)

*以上decorator修饰之后的函数,它们的_name_已经从原来的’now’变成了’wrapper’。因为返回的那个wrapper()函数名就是’wrapper’,所以需要把原始函数的_name_等属性复制到wrapper()函数中,否则,有些依赖函数名的代码执行就会出错。一个完整的decorator的写法如下:
import functools

def log(func):
@functools.wraps(func)
def wrapper(*args,**kw):
print(‘call %s():’ %func.name)
return func(args,kw)
return wrapper
在定义wrapper()的前面加上@functools.wraps(func)。
26.int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换。但int()函数还提供额外的base参数,默认值为10.如果传入base参数,就可以做N进制的转换。
int(‘12345’,base = 8) 5349
假设要转换大量的二进制字符串,每次都传入int(x,base=2)非常麻烦,于是可以定义一个int2()的函数,默认把base=2传进去:
def int2(x,base = 2):
return int(x,base)
而functools.partial就是帮助我们创建一个偏函数 的,不需要我们自己定义int2(),可直接使用下面的代码创建一个新的函数int2:
import functools
int2 = functools.partial(int,base = 2)
functools.partial的作用,就是把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个函数会更加简单。
可在函数调用时传入其他值: int2(‘10000’,base = 10) 10000
在创建偏函数时,实际上可以接收函数对象、*args和
kw这三个参数
(1) 当传入int2 = functools.partial(int,base = 2),实际上固定住了int()函数的关键字参数base,也就是int2(‘10010’) 相当于kw = {‘base’ : 2} int(‘10010’,**kw)
(2) 当传入 max2 = functools.partial(max,10),实际上会把10作为
args的一部分自动加到左边,也就是max2(5,6,7) 相当于 args = (10,5,6,7) max(*args)
27.读写文件是常见的IO操作。Python内置了读写文件的函数,用法与C是兼容的。
在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。
读文件
f = open(‘test.txt’,’r’) //标识符’r’表示读
f.read() //调用read()方法可以一次读取文件的全部内容,Python把内容读到内存,用一个str对象表示
f.close()//文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的。
为了保证无论是否出错都能正确地关闭文件,可使用try … finally来实现:
try:
f = open(‘test.txt’,’r’)
print(f.read())
finally:
if f:
f.close()
以上过于繁琐,Python引入with语句来自动帮我们调用close()方法:
with open(‘test.txt’,’r’) as f:
print(f.read())
*调用read()会一次性读取文件的全部内容,若文件过大,内存就爆了,所以保险起见,可反复调用read(size)方法,每次最多读取size个字节的内容。另外,调用readline()可以每次读取一行内容,调用readlines()一次性读取所有内容并按行返回list。
*遇到有些编码不规范的文件,可能会遇到UnicodeDecodeError,因为文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接收一个errors参数,表示如果遇到编码错误后如何处理。最简单的方法是直接忽略:f = open(‘test.txt’,’r’,encoding = ‘gbk’,errors = ‘ignore’)
前面默认读取文本文件,而且是UTF-8编码的文本文件。要读取二进制文件,如图片、视频等等,用’rb’模式打开文件即可:f = open(‘test.txt’,’rb’)
要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,比如读取GBK编码的文件:f = open(‘test.txt’,’r’,encoding = ‘gbk’)
写文件
写文件和读文件是一样的,唯一区别是调用open()函数时,传入标识符’w’或’wb’表示写文本文件或写二进制文件。要写入特定编码的文本文件,给open()函数传入encoding参数,将字符串自动转换成指定编码。
以’w’模式写入文件时,如果文件已经存在,会直接覆盖(相当于删除后重新写入一个文件)。如果希望追加到文件末尾,可传入’a’以追加(append)模式写入。
28.数据读写不一定是文件,也可以在内存中读写。StringIO和BytesIO是在内存中操作str
和bytes的方法,使得和读写文件具有一致的接口。
29.多进程
Unix/Linux系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:
import os
print(‘Process (%s) start…’ % os.getpid())

Only works on Unix/Linux/Mac:

pid = os.fork() //父进程内返回子进程的ID,子进程内返回0
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.
在Unix/Linux下,可以使用fork()调用多进程。
要实现跨平台的多进程,可以使用multiprocessing模块。
进程间通信是通过Queue、Pipes等实现的。
30.多线程
多任务可以由多进程完成,也可以由一个进程内的多线程完成。
进程是由若干线程组成的,一个进程内至少有一个线程。
由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:
import time, threading

新线程执行的代码:

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

print(‘thread %s is running…’ % threading.current_thread().name)
t = threading.Thread(target=loop, name=‘LoopThread’)
t.start()
t.join()
print(‘thread %s ended.’ % threading.current_thread().name)
执行结果如下:
thread MainThread is running…
thread LoopThread is running…
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……

*多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:
balance = balance + n
也分两步:
计算balance + n,存入临时变量中;
将临时变量的值赋给balance。
也就是可以看成:
x = balance + n
balance = x
因为修改balance需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。
两个线程同时一存一取,就可能导致余额不对,你肯定不希望你的银行存款莫名其妙地变成了负数,所以,我们必须确保一个线程在修改balance的时候,别的线程一定不能改。
如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:
balance = 0
lock = threading.Lock()

def run_thread(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。
获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try…finally来确保锁一定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。
但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。
不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
小结
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。
Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。

进程 vs. 线程


我们介绍了多进程和多线程,这是实现多任务最常用的两种方式。现在,我们来讨论一下这两种方式的优缺点。
首先,要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。
线程切换
无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?
我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。
如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。
假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。
但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
计算密集型 vs. IO密集型
是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
异步IO
考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单线程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。我们会在后面讨论如何编写协程。
31.分布式进程
Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。
注意Queue的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。

32.TCP/IP简介
虽然大家现在对互联网很熟悉,但是计算机网络的出现比互联网要早很多。
计算机为了联网,就必须规定通信协议,早期的计算机网络,都是由各厂商自己规定一套协议,IBM、Apple和Microsoft都有各自的网络协议,互不兼容,这就好比一群人有的说英语,有的说中文,有的说德语,说同一种语言的人可以交流,不同的语言之间就不行了。
为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,互联网协议簇(Internet Protocol Suite)就是通用协议标准。Internet是由inter和net两个单词组合起来的,原意就是连接“网络”的网络,有了Internet,任何私有网络,只要支持这个协议,就可以联入互联网。
因为互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议简称TCP/IP协议。
通信的时候,双方必须知道对方的标识,好比发邮件必须知道对方的邮件地址。互联网上每个计算机的唯一标识就是IP地址,类似123.123.123.123。如果一台计算机同时接入到两个或更多的网络,比如路由器,它就会有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。
IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。
IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。
IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334。
TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
端口有什么作用?在两台计算机通信时,只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个TCP报文来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。每个网络程序都向操作系统申请唯一的端口号,这样,两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。
一个进程也可能同时与多个计算机建立链接,因此它会申请很多端口。
了解了TCP/IP协议的基本概念,IP地址和端口的概念,我们就可以开始进行网络编程了。
33.TCP编程
TCP编程
阅读: 143322


Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
客户端
大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
举个例子,当我们在浏览器中访问新浪时,我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来的,后面的通信就是发送网页内容了。
所以,我们要创建一个基于TCP连接的Socket,可以这样做:

导入socket库:

import socket

创建一个socket:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

建立连接:

s.connect((‘www.sina.com.cn’, 80))
创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6。SOCK_STREAM指定使用面向流的TCP协议,这样,一个Socket对象就创建成功,但是还没有建立连接。
客户端要主动发起TCP连接,必须知道服务器的IP地址和端口号。新浪网站的IP地址可以用域名www.sina.com.cn自动转换到IP地址,但是怎么知道新浪服务器的端口号呢?
答案是作为服务器,提供什么样的服务,端口号就必须固定下来。由于我们想要访问网页,因此新浪提供网页服务的服务器必须把端口号固定在80端口,因为80端口是Web服务的标准端口。其他服务都有对应的标准端口号,例如SMTP服务是25端口,FTP服务是21端口,等等。端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。
因此,我们连接新浪服务器的代码如下:
s.connect((‘www.sina.com.cn’, 80))
注意参数是一个tuple,包含地址和端口号。
建立TCP连接后,我们就可以向新浪服务器发送请求,要求返回首页的内容:

发送数据:

s.send(b’GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n’)
TCP连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发,怎么协调,要根据具体的协议来决定。例如,HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。
发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收新浪服务器返回的数据了:

接收数据:

buffer = []
while True:
# 每次最多接收1k字节:
d = s.recv(1024)
if d:
buffer.append(d)
else:
break
data = b’’.join(buffer)
接收数据时,调用recv(max)方法,一次最多接收指定的字节数,因此,在一个while循环中反复接收,直到recv()返回空数据,表示接收完毕,退出循环。
当我们接收完数据后,调用close()方法关闭Socket,这样,一次完整的网络通信就结束了:

关闭连接:

s.close()
接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件:
header, html = data.split(b’\r\n\r\n’, 1)
print(header.decode(‘utf-8’))

把接收的数据写入文件:

with open(‘sina.html’, ‘wb’) as f:
f.write(html)
现在,只需要在浏览器中打开这个sina.html文件,就可以看到新浪的首页了。
服务器
和客户端编程相比,服务器编程就要复杂一些。
服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。
所以,服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
但是服务器还需要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理,否则,服务器一次就只能服务一个客户端了。
我们来编写一个简单的服务器程序,它接收客户端连接,把客户端发过来的字符串加上Hello再发回去。
首先,创建一个基于IPv4和TCP协议的Socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
然后,我们要绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,还可以用127.0.0.1绑定到本机地址。127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,也就是说,外部的计算机无法连接进来。
端口号需要预先指定。因为我们写的这个服务不是标准服务,所以用9999这个端口号。请注意,小于1024的端口号必须要有管理员权限才能绑定:

监听端口:

s.bind((‘127.0.0.1’, 9999))
紧接着,调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量:
s.listen(5)
print(‘Waiting for connection…’)
接下来,服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接:
while True:
# 接受一个新连接:
sock, addr = s.accept()
# 创建新线程来处理TCP连接:
t = threading.Thread(target=tcplink, args=(sock, addr))
t.start()
每个连接都必须创建新线程(或进程)来处理,否则,单线程在处理连接的过程中,无法接受其他客户端的连接:
def tcplink(sock, addr):
print(‘Accept new connection from %s:%s…’ % addr)
sock.send(b’Welcome!’)
while True:
data = sock.recv(1024)
time.sleep(1)
if not data or data.decode(‘utf-8’) == ‘exit’:
break
sock.send((‘Hello, %s!’ % data.decode(‘utf-8’)).encode(‘utf-8’))
sock.close()
print(‘Connection from %s:%s closed.’ % addr)
连接建立后,服务器首先发一条欢迎消息,然后等待客户端数据,并加上Hello再发送给客户端。如果客户端发送了exit字符串,就直接关闭连接。
要测试这个服务器程序,我们还需要编写一个客户端程序:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

建立连接:

s.connect((‘127.0.0.1’, 9999))

接收欢迎消息:

print(s.recv(1024).decode(‘utf-8’))
for data in [b’Michael’, b’Tracy’, b’Sarah’]:
# 发送数据:
s.send(data)
print(s.recv(1024).decode(‘utf-8’))
s.send(b’exit’)
s.close()
我们需要打开两个命令行窗口,一个运行服务器程序,另一个运行客户端程序,就可以看到效果了:
┌────────────────────────────────────────────────────────┐
│Command Prompt - □ x │
├────────────────────────────────────────────────────────┤
│$ python echo_server.py │
│Waiting for connection… │
│Accept new connection from 127.0.0.1:64398… │
│Connection from 127.0.0.1:64398 closed. │
│ │
│ ┌────────────────────────────────────────────────┴───────┐
│ │Command Prompt - □ x │
│ ├────────────────────────────────────────────────────────┤
│ │$ python echo_client.py │
│ │Welcome! │
│ │Hello, Michael! │
└───────┤Hello, Tracy! │
│Hello, Sarah! │
│$ │
│ │
│ │
└────────────────────────────────────────────────────────┘
需要注意的是,客户端程序运行完毕就退出了,而服务器程序会永远运行下去,必须按Ctrl+C退出程序。
小结
用TCP协议进行Socket编程在Python中十分简单,对于客户端,要主动连接服务器的IP和指定端口,对于服务器,要首先监听指定端口,然后,对每一个新的连接,创建一个线程或进程来处理。通常,服务器程序会无限运行下去。
同一个端口,被一个Socket绑定了以后,就不能被别的Socket绑定了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值