1.1 文件读写
1.2 StringIO和BytesIO
1.3 操作文件和目录
1.4 序列化
2. 进程和线程
2.1 多进程
2.2 多线程
2.3 ThreadLocal
2.4 进程 vs. 线程
2.5 分布式进程
1. IO编程
输入和输出:Input/Output。
同步与异步的区别就在于是否等待IO执行的结果。
回调模式和轮询模式。
1.1 文件读写
在磁盘上读写文件的功能是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象,然后,通过操作系统提供的接口从这个文件对象中读取数据,或者把数据写入这个文件。
读文件
>>> f = open('/Users/michael/test.txt', 'r') # 标示符r表示只读
>>> f.read() # 调用read()可以一次读取文件的全部内容
'Hello, world!'
>>> f.close() # 文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源
with open('/path/to/file', 'r') as f: # with语句会自动帮我们调用close()方法
print(f.read())
调用read()会一次性读取文件的全部内容,为了保险起见,可以反复调用read(size)方法,每次最多读取size个字节的内容,或者调用readline()可以每次读取一行内容,调用readlines()一次读取所有内容并按行返回list。
file-like Object
像open()返回的这种有read()方法的对象,在Python中统称为file-like Object。StringIO就是内存中创建的file-like Object,常用作临时缓冲。
二进制文件
>>> f = open('/Users/michael/test.jpg', 'rb') # 要读取二进制文件,比如图片,视频等等,用 rb 模式打开文件
>>> f.read()
b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六进制表示的字节
字符编码
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore') # encoding参数处理非UTF-8编码的文本文件,errors参数表示如果遇到编码错误后如何处理
>>> f.read()
'测试'
写文件
写文件和读文件是一样的,唯一区别就是调用open()函数时,传入标示符’w’或’wb’表示写文本文件或者写二进制文件。
>>> f = open('/Users/michael/test.txt', 'w')
>>> f.write('Hello, world!')
>>> f.close()
with open('/Users/michael/test.txt', 'w') as f: # 要写入特定编码的文本文件,在open()传入encoding参数,将字符串自动转换成指定编码
f.write('Hello, world!')
1.2 StringIO和BytesIO
StringIO:在内存中读写str。
# 写数据
>>> from io import StringIO
>>> f = StringIO()
>>> f.write('hello')
5
>>> f.write(' ')
1
>>> f.write('world!')
6
>>> print(f.getvalue()) # getvalue()方法用于获得写入后的str
hello world!
# 读数据
>>> from io import StringIO
>>> f = StringIO('Hello!\nHi!\nGoodbye!')
>>> while True:
... s = f.readline()
... if s == '':
... break
... print(s.strip())
...
Hello!
Hi!
Goodbye!
BytesIO:StringIO的操作只能是str,如果要操作二进制数据,就需要使用BytesIO。
# 写数据
>>> from io import BytesIO
>>> f = BytesIO()
>>> f.write('中文'.encode('utf-8'))
6
>>> print(f.getvalue())
b'\xe4\xb8\xad\xe6\x96\x87'
# 读数据
>>> from io import StringIO
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'
1.3 操作文件和目录
Python内置的os模块可以直接调用操作系统提供的接口函数。
>>> import os
# 操作系统类型,os模块的部分函数是跟操作系统有关的
>>> os.name
'posix'
# 获取系统的详细信息
>>> os.uname()
posix.uname_result(sysname='Darwin', nodename='MichaelMacPro.local', release='14.3.0', version='Darwin Kernel Version 14.3.0: Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64', machine='x86_64')
# 获取环境变量
>>> os.environ
environ({'VERSIONER_PYTHON_PREFER_32_BIT': 'no', 'TERM_PROGRAM_VERSION': '326', 'LOGNAME': 'michael', 'USER': 'michael', 'PATH': '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin', ...})
# 获取某个环境变量的值
>>> os.environ.get('PATH')
'/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin'
# 查看当前目录的绝对路径:
>>> os.path.abspath('.')
'/Users/michael'
# 在某个目录下创建一个新目录,首先把新目录的完整路径表示出来:
>>> os.path.join('/Users/michael', 'testdir')
'/Users/michael/testdir'
# 然后创建一个目录:
>>> os.mkdir('/Users/michael/testdir')
# 删掉一个目录:
>>> os.rmdir('/Users/michael/testdir')
合成两个路径的时候,不能直接拼接字符串,而是通过os.path.join()函数,这样可以正确的处理不同操作系统的路径分隔符。同理,拆分路径时,通过os.psth.split()函数,这样可以把一个路径拆分为两部分,后一部分总是最后级别的目录或文件名。
# 拆分目录路径
>>> os.path.split('/Users/michael/testdir/file.txt')
('/Users/michael/testdir', 'file.txt')
# 得到文件扩展名
>>> os.path.splitext('/path/to/file.txt')
('/path/to/file', '.txt')
文件操作
# 对文件重命名:
>>> os.rename('test.txt', 'test.py')
# 删掉文件:
>>> os.remove('test.py')
# 列出当前目录所有文件
>>> [x for x in os.listdir('.') if os.path.isdir(x)]
['.lein', '.local', '.m2', '.npm', '.ssh', '.Trash', '.vim', 'Applications', 'Desktop', ...]
1.4 序列化
我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫picking。序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。反过来把变量内容从序列化对象重新读到内存里称之为反序列化,即unpicking。
Python提供了pickle模块来实现序列化。
>>> import pickle
>>> d = dict(name='Bob', age=20, score=88)
# 把对象序列化并写入文件
>>> pickle.dumps(d)
b'\x80\x03}q\x00(X\x03\x00\x00\x00ageq\x01K\x14X\x05\x00\x00\x00scoreq\x02KXX\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.'
# pickle.dump()直接把对象序列化后写入一个file-like Object。
>>> f = open('dump.txt', 'wb')
>>> pickle.dump(d, f)
>>> f.close()
>>> f = open('dump.txt', 'rb')
# piclke.load()方法反序列化对象
>>> d = pickle.load(f)
>>> f.close()
>>> d
{'age': 20, 'score': 88, 'name': 'Bob'}
JSON
如果要在不同的编程语言之间传递对象,就必须要把对象序列化为标准格式。json表示出来是一个字符串,可以被所有语言读取,也可以方便的存储到磁盘或者通过网络传输。
Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。
>>> import json
>>> d = dict(name='Bob', age=20, score=88)
>>> json.dumps(d) # dumps()方法返回一个str,内容就是标准的json
'{"age": 20, "score": 88, "name": "Bob"}'
要把json反序列化为python对象,用loads()或者对应的load()方法,前者把json的字符串反序列化,后者从file-like Object中读取字符串并反序列化。
>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'
>>> json.loads(json_str)
{'age': 20, 'score': 88, 'name': 'Bob'}
2. 进程和线程
单核CPU可以执行多个任务,但每个任务都是交替执行的,因为CPU执行速度太快,感觉就像是同时执行一样。真正并发执行多任务只能在多核CPU上实现,由于任务数量远多于CPU的核心数量,所以操作系统会自动的把很多任务轮流调度到每个核心上执行。
对于操作系统,每一个任务就是一个进程。在一个进程内部,要同时干很多事情,就需要同时运行多个“子任务”,这些子任务称之为线程。
2.1 多进程
Unix/Linux操作系统提供了一个fork()系统调用,调用一次fork(),返回两次,因为操作系统把当前进程(父进程)复制一份(子进程),然后分别在子进程和父进程中返回。有了fork()调用,一个进程在接到新任务时就可以复制出一个子进程处理新任务。
常用的Apache服务器就是由父进程监听端口,每当有新的http请求,就fork子进程处理任务。
multiprocessing
multiprocessing模块就是跨平台版本的多进程模块,它提供了一个Process类来代表一个进程对象。
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.')
# 执行结果
Parent process 928.
Process will start.
Run child process test (929)...
Process end.
- 如果要启动大量的子进程,可以用进程池(Pool)的方式批量创建子进程。
- subprocess模块可以让我们非常方便的启动一个子进程,然后控制其输入和输出。
- multiprocessing模块包装了底层的机制,提供了queue,pipes等多种方式来交换数据达到进程间的通信。
2.2 多线程
Python标准库提供了两个模块,_thread(低级模块)和threaing(高级模块),一般使用threading。
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.
在多线程中,所有的变量为线程共享,任何一个线程都能修改变量。
# 经典存款案例
import time, threading
# 假定这是你的银行存款:
balance = 0
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
当多个线程同时执行的时候,只有一个线程能获得锁(Lock),然后继续执行代码,其他线程就继续等待直到获得锁为止。
锁的好处是确保了某段关键代码只有一个线程从头到尾完整的执行,坏处就是组织了多线程的运行,由于存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁,可能会造成死锁,导致多个线程同时挂起,既不能执行,也不能结束。
Python解释器由于设计时有GIL全局锁,导致多线程无法利用多核。
2.3 ThreadLocal
在多线程的环境下,每个线程都有自己的数据,一个线程使用自己的局部变量比使用全局变量好,局部变量只有线程自己能看见,不影响其他线程,而全局变量的修改必须加锁,局部变量在传递起来很麻烦。
import threading
# 创建全局ThreadLocal对象:
local_school = threading.local()
def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
# 执行结果
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程所有调用到的处理函数都可以非常方便的访问这些资源。
2.4 进程 vs. 线程
多进程模式的最大优点就是稳定性高,一个子进程崩溃了,不会影响到主进程和其他子进程,最大的缺点就是创建进程的代价大。
多线程模式通常会比多进程快一点,最大的缺点就是任何一个线程挂掉都有可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
多任务一旦到了一个限度(临界点),就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
任务的类型分为计算密集型和IO密集型。
- 计算密集型任务的特点就是要进行大量的计算,消耗CPU资源,要高效的利用cpu,计算密集型任务同时进行的数量应当等于cpu的核心数。
- IO密集型,涉及到网络,磁盘IO的任务都是IO密集型任务,CPU消耗少,任务大部分时间都是在等待IO操作完成,常见的大部分任务都是IO密集型任务。
充分利用操作系统提供的异步IO支持,就可以单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的服务器。在Python上,单进程的异步编程模型称为协程。
2.5 分布式进程
在多线程和多进程中优先选择多进程,因为进程更稳定,而且进程可以分布到多台机器上,而线程只能在同一台机器上的同一个cpu上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个子进程中,依靠网络通信。
Queue的作用是用来传递任务和接收结果,每个任务的描述数量量要尽量小。
感谢廖雪峰的官方网站提供的教程。Python学习笔记系列都基于廖老师的教程