Python文件操作和并发编程
6.1 模块
Python中每个.py文件就代表了一个模块,在不同的模块中可以有同名的函数,在使用函数的时候我们通过import关键字导入指定的模块,就可以区分到底要使用的是哪个模块中的函数。
6.1.1 什么是模块
逻辑上,模块就是一组功能函数的集合;物理上,一个模块就是一个包含了Python定义和声明的文件,文件名就是模块名字加上.py的后缀。在一个模块内部,模块名可以通过全局变量____name____的值获得。
6.1.2 模块的导入
使用import引入模块,导入模块中的某个函数需要from math import *
6.2 包
包是多个模块的集合。
6.2.1 目录结构
6.2.2 包的导入
from sound.effects import
6.3 文件
6.3.1 文件操作介绍
文件是系统存储区域的一个命名位置,能够在存储器中实现持续性存储。
f=open('file','mode')//打开文件
打开模式 | 说明 |
---|---|
‘r’ | 只读模式,默认值 |
‘w’ | 覆盖写模式,文件不存在则创建,存在则完全覆盖 |
‘x’ | 创建写模式 |
‘a’ | 追加写模式 |
‘b’ | 二进制文件模式 |
‘t’ | 文本文件模式,默认值 |
‘+’ | 同时读写功能 |
使用with关键字,优点是相关代码结束后文件会正确关闭,即使在某个时刻引发了异常。使用with比等效的try-finally代码块要简短得多。
with open('workfile') as f:
read_data=f.read()
f.closed
如果没有使用with关键字,我们不得不调用f.closed()来关闭文件并立即释放它使用的所有系统资源。如果我们没有显式的关闭文件,Python垃圾回收器最终将销毁该对象并为我们关闭相关文件,但这个关闭动作是有延迟的。
6.3.2 文件的相关函数
读取整个文件f.read(),逐行读取f.readline(),文件写入f.write()
//赋值图片功能
def main():
try:
with open('guido.jpg','rb') as fs1:
data=fs1.read()
print(type(data))//二进制类型
with open('吉多.jpg','wb') as fs2:
fs2.write(data)
except FileNotFoundError as e:
print('指定的文件无法打开')
except IOError as e:
print('读写文件时出现错误')
if _name_=='_main_':
main()
读取文件的方式有两种,一次性全文本读取或逐行读取。写入文件的方式也有两种,一次性全文本修改或逐行修改。
6.4 用json模块存储数据
在程序关闭之前,我们需要把这些有价值的数据保存下来。文本文件很擅长处理字符串类型的数据或二进制数据,但面对字典或嵌套列表之类的复杂数据结构就力不从心了。
json是一种轻量级的数据交换格式,易于读写同时易于机器解析生成。Python中的json模块能将简单的json数据结构转储到文件中,并在程序再次运行时加载该文件中的json数据。json这种数据类型简直就是为了设计API而诞生的。
import json
numbers=[2,3,5,7,11,13]
filename='numbers.json'
with open(filename,'w') as f:
json.dump(numbers,f)//存储数字列表
with open(filename) as f2:
numbers2=json.load(f2)
print(numbers2)
6.5 文件读写的具体应用
6.5.1 读写文本数据
f=open('somefile.txt','rt')
data=f.read()
f.close()
6.5.2 打印输出至文件中
with open('test.txt','wt') as f:
print('hello world',file=f)
6.5.3 读写二进制字节数据
with open('somefile.bin','rb') as f:
data=f.read(16)
text=data.decode('utf-8')
with open('somefile.bin','wb') as f:
text='hello world'
f.write(text.encode('utf-8'))
6.5.4 字符串IO操作
IO就是在内存中读写字符串。
s=io.StringIO()
s.write('hello world')
s.read(4)//hell
s.read()//o/nworld/n
6.5.5 读写压缩文件
读写gzip或bz2格式的压缩文件,系统自带的gzip或bz2模块可处理这些文件。
import gzip
import bz2
with gzip.open('somefile.gz','rt') as f:
text=f.read()
with bz2.open('somefile.bz2','rt') as f:
text=f.read()
with gzip.open('somefile.gz','wt') as f:
f.write(text)
6.5.6 内存映射的二进制文件
内存映射一个二进制文件到一个可变字节数组中,目的可能是为了随机访问它的内容或是原地做些修改。使用mmap模块来内存映射文件。
import os
import mmap
def memory_map(filename,access=mmap.ACCESS_WRITE):
size=os.path.getsize(filename)
fd=os.open(filename,os.O_RDWR)
return mmap.mmap(fd,size,access=access)
size=1000000
with open('data','wb') as f:
f.seek(size-1)
f.write(b'\x00')
m=memory_map('data')
len(m)
with open('data','rb') as f:
print(f.read(11))
6.6 数据组织维度
6.6.1 一维数据
一维数据由对等关系的有序或无序数据构成,采用线性方式组织,对应数学中数组的概念。Python中主要采用列表形式表示。
一维数据存储有多种方式,用,做分隔的存储格式称CSV格式,它是一种通用的相对简单的文件格式,各元素用逗号分隔形成一行。Python中,需要将列表对象输出为CSV格式以及将CSV格式读入成列表对象。
ls=['北京','上海','天津','重庆']
f=open("city.csv","w")
f.write(",".join(ls)+"\n")
f.close()
f=open("city.csv","r")
ls=f.read().strip('\n').split(',')
f.close()
print(ls)
6.6.2 二维数据
二维数据也称表格数据,由关联关系数据构成,采用二维表格方式组织,常见的表格都属于二维数据。二维数据可采用二维列表来表示,可以使用CSV格式文件存储。二维数据处理等同于二维列表的操作,二维列表一般需要借助循环遍历实现对每个数据的处理。
ls=[
['指标','2014年','2015年','2016年'],
['居民消费价格指数','102','101.4','102'],
['食品','103.1','102.3','104.6'],
['烟酒及用品','994','102.1','101.5'],
['衣着','102.4','102.7','101.4']
]
f=open("cpi.csv","w")
for row in ls:
f.write(",".join(row)+"\n")
f.close()
f=open("cpi.csv","r")
ls=[]
for line in f:
ls.append(line.strip('\n').split(","))
f.close()
print(ls)
二维数组是数组中的数组,也是一个数组的数组。在这种类型的数组中,数据元素的位置由两个索引、而不是一个索引来引用。所以二维数组表示了一个包含行和列的数据的表,它比一维数据拥有更丰富的表达形式。
6.6.3 多维数据
在数据挖掘或机器学习时,我们面对的数据往往是高维数据。高维数据提供了更多的信息和细节,也更好地描述了样本,但高效且准确的分析方法也将无法使用。解决这个问题的方法便是降低数据的维度,在数据降维时,要使用尽量少的维度来表达较多原数据的特性和结构。
9.1 线程和进程的概念
进程就是操作系统中执行的一个程序,操作系统以进程为单位分配存储空间,每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据等。操作系统管理所有进程的运行,为它们合理地分配资源。进程可以通过分叉fork的方式来创建新的进程执行任务,不过新的进程也需要独立的内存空间,因此必须通过进程间通信机制来实现数据通信和共享,具体的方式包括管道、信号、套接字、共享内存区等。
一个进程还可以拥有多个并发的执行子程序,也就是拥有多个可以获得CPU调度的执行单元,这些执行单元就是所谓的线程。由于诸多线程在同一个进程下,它们可以共享相同的上下文资源,因此相对于进程而言,线程间的信息共享和通信更容易些。当然在单核CPU系统中,真正意义上的并发是不存在的,因为在某个时间点上能够获得CPU资源的有且仅有一个线程,多个线程通过轮询的方式共享了CPU的执行时间。多线程实现并发编程为大型软件或者手机APP上或多或少都用到了多线程技术。
当然多线程也并非没有缺点,从其他进程的角度来看,多线程的程序对其他程序非常霸道,因为它占用了更多的CPU资源,挤占其他程序获得的CPU执行时间;另一方面,站在开发者角度,编写和调试多线程的程序都对开发者有较高的要求,对于初学者来说则更加不友好。
总结,进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。一个应用程序至少包括一个进程,而一个进程则对应一个或多个线程,线程的尺度更小。每个进程在执行过程中拥有独立的内存空间,而一个进程的多个线程在执行过程中是共享内存的。对于Python而言,它既支持多进程又支持多线程。
9.2 多进程和多线程
我们考虑是否采用多进程或多线程的原因取决于任务的类型,任务的类型可以分为计算密集型和I/O密集型。计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如对视频进行编码解码或者格式转换和最近很火的比特币挖矿等,这些任务全靠CPU的运算能力,虽然也可以用多任务完成,但是任务越多,花在任务切换的时间和资源就越多,CPU执行任务的效率就越低。计算密集型任务由于主要消耗CPU资源,这类任务用Python这样的脚本语言去执行效率通常很低,最能胜任这类任务的是C语言,不过在有些项目中也会考虑使用Python中的嵌入C代码的机制。除了计算密集型任务,其他与此相关的网络、存储介质I/O的任务都可以视为I/O密集型任务,这类任务的特点是CPU消耗很少,因为I/O的速度远远低于CPU和内存的速度,所以此类任务的大部分时间都在等待I/O操作完成。对于I/O密集型任务,这其中包括了网络应用、web应用、和数据库开发等。
现代操作系统对I/O操作的支持中最重要的就是异步I/O,它充分利用操作系统提供的异步I/O支持就可以用单进程单线程模型来完成多任务,这种全新的模型称为事件驱动模型。大名鼎鼎的中间件Nginx就是支持异步I/O的web服务器,它在单核CPU上采用单进程模型高效地支持多任务。在多核CPU上,可以运行多个进程,充分利用多核CPU的优势。用Node.js开发的服务器端程序也经常使用这种工作模式,这也是未来实现多任务编程的大趋势。
在Python语言中,单线程+异步I/O的编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。协程最大的优势就是极高的执行效率,因为子程序切换不是线程切换,而是由于程序自身控制,因此没有线程切换的开销。协程的第二个优势就是不需要多线程的锁机制,因为只有一个线程,也不存在共享变量的冲突,在协程中控制共享资源不用加锁,只需要判断状态就好了,所以执行效率比多线程要高很多。想要充分利用CPU多核特性,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能提升。
9.3 多进程实践
multiprocessing模块是一个类似于threading模块的接口,它对进程的各种操作进行了良好的封装,并提供了进程之间通信所需要的接口,例如Pipe、Queue等,帮助我们实现进程间的通信和同步等操作。
9.3.1 multiprocessing模块
multiprocessing是一个用于创建或管理进程的模块。它同时对本地并发和远程并发提供支持,使用子进程代替线程,有效避免了GIL锁带来的不良影响。multiprocessing模块可运行于UNIX、Linux和Windows上,充分利用服务器上的多核来完成任务。
在multiprocessing中,我们是通过创建一个process对象并调用它的start()方法来创建和启动进程的。
from multiprocessing import Process
def f(name):
print('hello',name)
if _name_=='_main_':
p=Process(target=f,args=('bob',))
p.start()
p.join()
from multiprocessing import Process
import os
def run_proc(name):
print('Run child process %s (%s)')% (name,,os.getpid())
if _name_=='_main_':
print('Parent process %s.' % os.getpid())
p=Process(target=run_proc,args=('test',))
print('Child process will start.')
p.start()
p.join()
print('Child process end.')
9.3.2 进程池
如果存在上百甚至上千个目标进程需求,可以用到multiprocessing模块提供的Pool对象。初始化进程池Pool时,能够指定一个最大进程数,当有新的请求提交到进程池Pool中时,如果池子还没有满,就会创建一个新的进程来执行该请求,但如果池中的进程数已经达到指定的最大阈值,那么该请求就会悬停,直到池中有进程结束后释放相关的资源才会创建新的进程来执行任务请求。
from multiprocessing import Pool
import random,time
def work(num):
print(random.random()*num)
time.sleep(3)
if _name_ == '_main_':
po=Pool(3)#最大进程数为3,默认大小为CPU核数
for i in range(10):
po.apply_async(work,(i,))#apply_async选择要调用的目标
po.close()
po.join()
使用Pool时,如果不指定进程数量,则最大进程数会默认为CPU核心数量。CPU的核心数量对应的是计算机的逻辑处理器数量而不是内核数量,这点很多人搞错了。进程数量可以是成百上千,只要用Pool(10)就可以同时开启10个进程进行抓取。无论多进程还是多线程,数量开启太多都会造成切换费时,降低效率,所以需要慎重考虑是否需要创建这么多线程或进程。
9.3.3 进程间通信
Queue是多进程的队列,使用Queue能实现多进程之间的数据传递。通过put方法把数据插入到队列的尾部通过get方法从队列头部取出数据,且它们都有两个参数,分别为blocked和timeout。当队列成员已满且blocked参数为True时,如果timeout为正值,则put方法会阻塞一定的时间,直到该队列腾出新的空间。如果超时,会抛出Queue.Full的异常。同理,当队列为空切且blocked为True时,如果timeout为正值,则会等待直到有数据插入再取走。若在等待时间内没有数据插入,则会抛出Queue.Empty异常。
from multiprocessing import Process,Queue
def addone(q):
q.put(1)
def addtwo(q):
q.put(2)
if _name_=='_main_':
q=Queue()
p1=Process(target=addone,args=(q,))
p2=Process(target=addtwo,args=(q,))
p1.start()
p2.start()
p1.join()
p2.join()
print(q.get())
pritn(q.get())
Pipe对象分两种,一种为单向管道,一种为双向管道。默认双向管道,但可以通过构造方法Pipe(duplex=False)来创建单向管道。Pipe执行任务的方式是,一个进程从Pipe的一端输入对象,然后一个进程从Pipe的另一端接收对象,就像一个输油管道一样。单向管道只允许管道一端的进程输入,而双向管道则允许从两端输入。
import random
import time
from nultiprocessing import Process,Pipe,current_process
def produce(conn):
while True:
new=random.randint(0,100)
print('{}produce{}'.format(current_process().name,new))
conn.send(new)
time.sleep(random.random())
def consume(conn):
while True:
print('{}consume{}'.format(current_process().name,conn.recv()))
time.sleep(random.random())
if _name_=='_main_':
pipe=Pipe()
p1=Process(target=produce,args=(pipe[0],))
p2=Process(target=consume,args=(pipe[1],))
p1.start()
p2.start()
Queue使用put和get来维护队列,Pipe使用send recv来维护队列;Pipe只提供两个端点,而Queue没有限制。这就表示使用Pipe时只能同时开启两个进程,一个生产者,一个消费者,它们分别对这两个端点操作,两个端点共同维护一个队列。如果多个进程对pipe的同一个端点同时操作,就会发生错误,因为没有上锁。所以两个端点就相当于只提供两个进程安全的操作位置,这个机制限制了进程数量只能是2。Queue的封装更好,Queue只提供一个结果,它可以被很多进程同时调用;而Pipe返回两个结果,要分别被两个进程调用,Queue的实现基于Pipe的底层,所以Pipe的运行速度比Queue快很多。当只需要两个进程时,使用Pipe更快;当需要多个进程同时操作队列时,推荐使用Queue。
9.5 并发编程分类
第一种是多线程编程。Python中通过threading模块的Thread类并辅以Lock 、Condition等类来支持多线程编程。Python解释器通过GIL全局解释器锁来防止多个线程同时执行本地字节码,这个锁对于CPython是必需的,因为CPython的内存管理并不是以线程安全为首要目标的。GIL的存在导致Python的多线程并不能利用CPU的多核特性。
第二种是多进程编程。使用多进程能缓解GIL带来的性能瓶颈问题。Python中的multiprocessing模块提供了Process类来实现多进程,其他的辅助类和threading模块中的类相似,由于在操作层面对进程的保护,进程间的内存是相互隔离的。进程间通信和共享数据必须使用管道、套接字等方式,这一点从编程人员的角度来讲是比较麻烦的。为此,Python的multiprocessing模块提供了一个名为queue的类,它基于管道和锁机制提供了多个进程共享的队列。
第三种是异步编程,也叫异步I/O。所谓异步编程是通过调度程序从任务队列中挑选任务,调度程序以交叉的形式执行这些任务,我们不能保证任务将以某种顺序执行,因为执行顺序取决于队列中的一项任务是否愿意将CPU处理时间让位给另一项任务。异步编程通常通过多任务协作处理的方式实现,由于执行时间和顺序不确定,因此需要通过回调函数或Future对象来获取任务执行结果。Python3使用asyncio模块以及await和async关键字提供了对异步I/O的支持。
import asyncio
async def fetch(host):
#从指定站点抓取信息(协程函数)
print(f'start fetching {host} \n')
#跟服务器建立连接
reader,writer=awaitasyncio.open_connection(host,80)
#构造请求行和请求头
writer.write(b'GET/HTTP/1.1\r\n')
writer.write(f'Host:{host}\r\n'.encode())
writer.write(b'\r\n')
#清空缓存区(发送请求)
await writer.drain()
#接收服务器响应(读取响应行和响应头)
line=await reader.readline()
while line!=b'\r\n':
print(line.decode().rstrip())
line=await reader.readline()
print('\n')
writer.close()
def main():
urls=('www.sohu.com','www.douban.com','www.163.com')
#获取系统默认的事件循环
loop=asyncio.get_event_loop()
#用生成式语法构造一个包含多个协程对象的列表
tasks=[fetch(url) for url in urls]
#通过asyncio模块的wait函数将协程列表包装成task并等待其执行完成
#通过事件循环的run_until_complete方法运行任务直到完成并返回它的结果
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
if _name_=='_main_':
main()
如果程序不需要真正的并发性,而是依赖于异步处理和回调时,异步I/O就是一种很好的选择;另一方面,当程序中有大量等待与休眠进程时,也应该考虑使用异步I/O。对于一次I/O操作,数据会先被复制到操作系统内核的缓冲区中,然后从操作系统内核的缓冲区复制到应用程序的缓冲区,最后交给进程。
1.阻塞I/O:进程发起读操作,如果内核数据尚未就绪,进程会阻塞等待数据直到内核数据就绪并复制到进程的内存中。
2.非阻塞I/O:进程发起读操作,如果内核数据尚未就绪,进程不阻塞而是收到内核返回的错误信息,进程收到错误信息可以再次发起读操作,一旦内核数据准备就绪,就立即将数据复制到用户内存中,然后返回。
3.多路复用I/O:监听多个I/O对象,当I/O对象有变化(数据就绪)时就通知用户进程。多路复用的优势并不在于单个I/O操作能处理的更快,而是在于能处理更多的I/O操作。
4.异步I/O:进程发起读操作后就可以去做别的事情了,内核收到异步读操作后会立即返回,所以用户进程不阻塞,当内核数据准备就绪时,内核发送一个信号给用户进程,告诉它读操作完成了。
9.6 线程池
使用线程池可以很好地提升性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。此外,使用线程池可以有效控制系统中并发线程的数量。当系统中包含大量并发线程时,会导致系统性能急剧下降,甚至导致Python解释器崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。
from concurrent.futures import ThreadPoolExecutor
import urllib.request
def fetch_url(url):
u=urllib.request.urlopen(url)
data=u.read()
return data
pool=ThreadPoolExecutor(10)
a=pool.submit(fetch_url,'http://www.python.org')
b=pool.submit(fetch_url,'http://www.pypy.org')
x=a.result()
y=b.result()