文章目录
- 文件IO
-
- 问题:如何读写文本文件
- 问题:如何处理二进制文件
- 问题:如何设置文件的缓冲
- 问题:如何将文件映射到内存
- 问题:如何访问文件的状态
- 问题:如何使用临时文件
- 类与对象
-
- 问题:如何派生内置不可变类型
- 问题:如何为创建大量实例节省内存
- 问题:如何让对象支持上下文管理
- 问题:如何创建可管理的对象属性
- 如何让类支持比较操作
- 问题:如何使用描述符对实例属性做类型检查
- 问题:如何在环状数据结构中管理内存
- 问题:如何通过方法名的字符串调用方法
- 多线程并发
-
- 问题:如何使用多线程
- 问题:如何实现线程间通信
- 问题:如何实现线程间事件通知
- 问题:如何使用线程本地数据
- 问题:如何使用线程池
- 问题:如何使用多进程
- 装饰器
-
- 问题:如何使用装饰器
- 问题:如何保存原函数的信息
- 问题:如何定义带参数的装饰器
- 问题:如何实现属性可修改的函数装饰器
- 问题:如何在类中定义装饰器
- 小结
文件IO
问题:如何读写文本文件
- 这里需要关注一个变化
- 也就是说,在python3中所有文本默认实用Unicode编码,无需加
u''
,str
类型
- python2中读写文件需要解码编码(因为是字节串),python3中默认以
t
方式打开文件(真正的字符串),可以通过encoding
指明编码即可,更便捷
问题:如何处理二进制文件
问题:如何设置文件的缓冲
- 为了减少IO操作次数(费时且按块执行),需要设置缓冲
- 一般分为全缓冲、行缓冲、无缓冲
f = open('opera.txt', 'wb')
f.write(b'abc')
f.write(b'a'*4093)
f.write(b'b')
- 当我们已
t
默认模式打开的时候,会发现写了4096以上的字符仍然没有刷盘
- 这是因为python3中的t模式其实是基于二进制模式的,他的缓冲容量为8192
- 以上是文本模式的全缓冲;行缓冲是遇到换行就读入,常见的bash就是行缓冲模式,一个命令敲回车就执行
- 使用python,可以指定无缓冲
f = open('opera.txt', 'wb', buffering=0)
f.write(b'abc')
问题:如何将文件映射到内存
- 实际案例:
- 大文件都是保存在磁盘中的(使用时装载,操作系统决定),如果希望能统一加载到内存操作
- 使用标准库中的
mmap
,将文件映射到进程的内存地址空间;具体操作可以查资料,知道有这么一种场景即可!
问题:如何访问文件的状态
问题:如何使用临时文件
- 有事保存了大量的临时数据做数据分析,使用后依旧占用空间
from tempfile import TemporaryFile
tf = TemporaryFile()
tf.write(b'*'*4096)
tf.seek(0)
print(tf.read(512))
tf.close()
- 有名字的临时文件,多进程想同时使用
from tempfile import NamedTemporaryFile
ntf = NamedTemporaryFile(delete=False)
print(ntf.name)
import tempfile
print(tempfile.gettempprefix())
- 这一部分还是遇到实际问题更容易理解一些!了解即可
类与对象
问题:如何派生内置不可变类型
- 实战案例:
- 我们可以自定义这个类,在构造方法中过滤,然后传递给父类的构造方法
class IntTuple(tuple):
def __init__(self, iter):
int_tup = (i for i in iter if isinstance(i, int) and i>0)
super().__init__(int_tup)
- 这里会出现问题,
- 区别是这样的
- 所有的对象归根结底都是有
object
类的__new__()
方法创建,这个方法使用cls
(我们定义的类名)作为参数(源)
- 我们观察下面的显式调用的例子就懂了:
- 也就是说,有的类(list)会经过new和init两个方法产生对象;一般是在init中,所以我们也叫它构造方法
- 有的类(tuple)在new的时候就已经产生了对象,根本没有实现init方法(
is
父类的init方法)class IntTuple(tuple):
def __new__(cls, iter):
int_tup = (i for i in iter if isinstance(i, int) and i>0)
return super().__new__(cls, int_tup)
it = IntTuple(['aa', 1, 2, 3, '6'])
print(it)
问题:如何为创建大量实例节省内存
- 案例:
- 这里用到
__slots__
变量class Person1():
def __init__(self, name, uuid, age):
self.name = name
self.uuid = uuid
self.age = age
class Person2():
__slots__ = ['name', 'uuid', 'age']
def __init__(self, name, uuid, age):
self.name = name
self.uuid = uuid
self.age = age
p1 = Person1('Roy', 1002, 18)
p2 = Person2('Roy', 1002, 18)
alist = set(dir(p1))-set(dir(p2))
print(alist)
p1.w = 120
print(p1.__dict__)
import sys
print(sys.getsizeof(p1.__dict__))
print(sys.getsizeof(p1.name))
print(sys.getsizeof(p1.uuid))
print(sys.getsizeof(p1.age))
print(sys.getsizeof(p1.w))
__dict__
中存储的就是成员变量和定义的临时变量
- 定义了
__slot__
就无法添加临时变量,关闭动态绑定属性!
- 怎么证明这一点呢?
import tracemalloc
tracemalloc.start()
p2 = [Person2(1,2,3) for _ in range(10000)]
snap = tracemalloc.take_snapshot()
top = snap.statistics('filename')
for i in top[:10]:
print(i)
问题:如何让对象支持上下文管理
- 常见的上下文管理器就是
with
,可以优雅的打开和关闭文件
- 工作流程解析:其实是调用了
__enter__
和__exit__
方法
- 同样的,我们可以借助
with
并自定义上述方法实现自己的上下文管理
问题:如何创建可管理的对象属性
- 方法是对象的接口,一般获取属性也使用方法
- 使用方法繁琐,直接获取属性太暴露;所以希望在形式上是直接访问属性,实际上内部调用的是方法
- 简单+安全,使用
property
实现import math
class Circle():
def __init__(self,radius):
self.radius = radius
def get_r(self):
return round(self.radius,2)
def set_r(self, r):
if not isinstance(r, (int, float)):
raise TypeError('请输入整数或者小数')
self.radius = r
@property
def S(self):
return self.radius**2 * math.pi
@S.setter
def S(self, s):
self.radius = math.sqrt(s/math.pi)
R = property(get_r, set_r)
c = Circle(3)
print(c.R)
c.R = 4
print(c.R)
print(c.S)
print(c.R)
c.S = 80
print(c.S)
print(c.R)
- 上面通过两种方法通过property创建可管理属性,安全便捷
如何让类支持比较操作
- 很简单,根据需求,对比较运算符进行重载
- 这里使用
total_ordering
,只需定义两个比较方法,就能自动推算,无需重写所有!from functools import total_ordering
@total_ordering
class Rect():
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
def __lt__(self, obj):
return self.get_area() < obj.get_area()
def __eq__(self, obj):
return self.get_area() == obj.get_area()
r1 = Rect(2,3)
r2 = Rect(3,4)
print(r1 < r2)
print(r1 <= r2)
print(r1 >= r2)
- 如果我们定义类时忘了定义area之类的方法,就尴尬了,可以使用继承的方法限定
from functools import total_ordering
from abc import ABCMeta
@total_ordering
class Shape(metaclass=ABCMeta):
@classmethod
def get_area(self):
pass
def __lt__(self, obj):
return self.get_area() < obj.get_area()
def __eq__(self, obj):
return self.get_area() == obj.get_area()
class Rect(Shape):
def __init__(self, w, h):
self.width = w
self.height = h
def get_area(self):
return self.width * self.height
def __lt__(self, obj):
return self.get_area() < obj.get_area()
def __eq__(self, obj):
return self.get_area() == obj.get_area()
r1 = Rect(2,3)
r2 = Rect(3,4)
print(r1 < r2)
print(r1 <= r2)
print(r1 >= r2)
问题:如何使用描述符对实例属性做类型检查
- 这个描述符其实就是自定义一个重写了一些方法的类,可以在业务类中校验属性
class Desc:
def __init__(self, key):
self.key = key
def __set__(self, instance, value):
'''
:param instance: 实例化的类对象
:param value: 设置的值
:return:
'''
instance.__dict__[self.key] = value
def __get__(self, instance, owner):
print('get', instance,owner)
return instance.__dict__[self.key]
def __delete__(self, instance):
del instance.__dict__[self.key]
class Student:
name = Desc('Roy')
age = Desc(18)
- 好的,改写一下,做类型校验,不过要注意这里的类定义,不需再按照传统方式
class Desc:
def __init__(self, key, _type):
self.key = key
self._type = _type
def __set__(self, instance, value):
'''
:param instance: 实例化的类对象
:param value: 设置的值
:return:
'''
if not isinstance(value, self._type):
raise TypeError('不是想要的类型')
instance.__dict__[self.key] = value
def __get__(self, instance, owner):
print('get', instance,owner)
return instance.__dict__[self.key]
def __delete__(self, instance):
del instance.__dict__[self.key]
class Student:
name = Desc('Roy', str)
age = Desc(18, int)
s = Student()
s.name = 'roy'
s.age = 23
s.name = 18
print(s.name)
问题:如何在环状数据结构中管理内存
- 循环引用会让对象垃圾不能被及时回收
- 可以通过
__del__
方法查看析构的过程,通过下面的例子可以证明循环引用带来的问题class Node:
'''
双向链表,链表:抓住一个节点就OK
'''
def __init__(self, data):
self.data = data
self.right = None
self.left = None
def add_right(self, node):
self.right = node
node.left = self
def __str__(self):
return 'Node:<%s>'%self.data
def __del__(self):
print('__del__',self)
def double_link(n):
'''
常见有n个节点的列表
:param n:
:return:
'''
head = current = Node(1)
for i in range(2, n+1):
node = Node(i)
current.add_right(node)
current = node
return head
head = double_link(20)
head = None
import time
for i in range(5):
time.sleep(1)
print('run...')
- 左指针使用弱引用,不增加引用计数;注意属性访问方式的变化(使用property,属性变函数,简单安全)
import weakref
class Node:
'''
双向链表,链表:抓住一个节点就OK
'''
def __init__(self, data):
self.data = data
self.right = None
self._left = None
def add_right(self, node):
self.right = node
node.left = weakref.ref(self)
@property
def left(self):
return self._left()
def __str__(self):
return 'Node:<%s>'%self.data
def __del__(self):
print('__del__',self)
def double_link(n):
'''
常见有n个节点的列表
:param n:
:return:
'''
head = current = Node(1)
for i in range(2, n+1):
node = Node(i)
current.add_right(node)
current = node
return head
head = double_link(20)
head = None
import time
for i in range(5):
time.sleep(1)
print('run...')
问题:如何通过方法名的字符串调用方法
多线程并发
问题:如何使用多线程
- 例如要下载某个接口的数据并保存,可以使用多线程加速
- 我们通过访问网站下载CSV资源来说明问题
import requests
import base64
from io import StringIO
import csv
from xml.etree.ElementTree import ElementTree, Element, SubElement
USERNAME = b'7f304a2df40829cd4f1b17d10cda0304'
PASSWORD = b'aff978c42479491f9541ace709081b99'
def download_csv(page_number):
print('download csv data [page=%s]' % page_number)
url = "http://api.intrinio.com/prices.csv?ticker=AAPL&hide_paging=true&page_size=200&page_number=%s" % page_number
auth = b'Basic ' + base64.b64encode(b'%s:%s' % (USERNAME, PASSWORD))
headers = {'Authorization' : auth}
response = requests.get(url, headers=headers)
if response.ok:
return StringIO(response.text)
def csv_to_xml(csv_file, xml_path):
print('Convert csv data to %s' % xml_path)
reader = csv.reader(csv_file)
headers = next(reader)
root = Element('Data')
root.text = '\n\t'
root.tail = '\n'
for row in reader:
book = SubElement(root, 'Row')
book.text = '\n\t\t'
book.tail = '\n\t'
for tag, text in zip(headers, row):
e = SubElement(book, tag)
e.text = text
e.tail = '\n\t\t'
e.tail = '\n\t'
ElementTree(root).write(xml_path, encoding='utf8')
def download_and_save(page_number, xml_path):
csv_file = None
while not csv_file:
csv_file = download_csv(page_number)
csv_to_xml(csv_file, 'data%s.xml' % xml_path)
from threading import Thread
class MyThread(Thread):
def __init__(self, page_number, xml_path):
super().__init__()
self.page_number = page_number
self.xml_path = xml_path
def run(self):
download_and_save(self.page_number, self.xml_path)
if __name__ == '__main__':
import time
t0 = time.time()
thread_list = []
for i in range(1, 6):
t = MyThread(i, 'data%s.xml' % i)
t.start()
thread_list.append(t)
for t in thread_list:
t.join()
print(time.time() - t0)
print('main thread end.')
- 在主要的
download_and_save
函数中,下载属于IO密集型操作,转换属于CPU密集操作
- 多线程只能加速IO操作,可以同时请求并下载(无需CPU过多参与),但不能加速CPU密集操作,除非多核并行(但这也不叫多线程加速了,属于多进程)
问题:如何实现线程间通信
- 上面说不能加速CPU密集型运算,为什么呢?
- 这是由于全局解释器锁(GIL)的存在
- 在同一进程(当前任务)的各个线程中,当我们给每个线程分配一个CPU核加速运算,各线程都要先拿到这个GIL才能执行,所以多核和一核并无区别
- 对IO操作是没有这个限制的,GIL锁会自动释放
- 可以用一个线程和两个线程试一下,使用
top
命令监控CPU状态
- 多进程是独立的,不受影响
- 更改一下上面的代码架构
- 多个线程下载资源,使用标准库中的
queue.Queue
(线程安全的),放入队列
- 一个线程进行转换,从队列中取数据(生产者-消费者模型)
- 线程安全的意思是
Queue
自带了加锁机制,多线程访问时可以避免混乱import requests
import base64
from io import StringIO
import csv
from xml.etree.ElementTree import ElementTree, Element, SubElement
from threading import Thread
USERNAME = b'7f304a2df40829cd4f1b17d10cda0304'
PASSWORD = b'aff978c42479491f9541ace709081b99'
class DownloadThread(Thread):
def __init__(self, page_number, queue):
super().__init__()
self.page_number = page_number
self.queue = queue
def run(self):
csv_file = None
while not csv_file:
csv_file = self.download_csv(self.page_number)
self.queue.put((self.page_number, csv_file))
def download_csv(self, page_number):
print('download csv data [page=%s]' % page_number)
url = "http://api.intrinio.com/prices.csv?ticker=AAPL&hide_paging=true&page_size=200&page_number=%s" % page_number
auth = b'Basic ' + base64.b64encode(b'%s:%s' % (USERNAME, PASSWORD))
headers = {'Authorization' : auth}
response = requests.get(url, headers=headers)
if response.ok:
return StringIO(response.text)
class ConvertThread(Thread):
def __init__(self, queue):
super().__init__()
self.queue = queue
def run(self):
while True:
page_number, csv_file = self.queue.get()
self.csv_to_xml(csv_file, 'data%s.xml' % page_number)
def csv_to_xml(self, csv_file, xml_path):
print('Convert csv data to %s' % xml_path)
reader = csv.reader(csv_file)
headers = next(reader)
root = Element('Data')
root.text = '\n\t'
root.tail = '\n'
for row in reader:
book = SubElement(root, 'Row')
book.text = '\n\t\t'
book.tail = '\n\t'
for tag, text in zip(headers, row):
e = SubElement(book, tag)
e.text = text
e.tail = '\n\t\t'
e.tail = '\n\t'
ElementTree(root).write(xml_path, encoding='utf8')
from queue import Queue
if __name__ == '__main__':
queue = Queue()
thread_list = []
for i in range(1, 6):
t = DownloadThread(i, queue)
t.start()
thread_list.append(t)
convert_thread = ConvertThread(queue)
convert_thread.start()
for t in thread_list:
t.join()
print('main thread end.')
- 我们分别定义下载线程和转换线程,使用队列作为参数
- 思考:线程安全如何实现的?
问题:如何实现线程间事件通知
- 接着上面的任务
- 我们可以使用
threading.Event
,并使用tarfile
包,增加一个打包的线程import requests
import base64
from io import StringIO
import csv
from xml.etree.ElementTree import ElementTree, Element, SubElement
from threading import Thread
from queue import Queue
import tarfile
import os
USERNAME = b'7f304a2df40829cd4f1b17d10cda0304'
PASSWORD = b'aff978c42479491f9541ace709081b99'
class DownloadThread(Thread):
def __init__(self, page_number, queue):
super().__init__()
self.page_number = page_number
self.queue = queue
def run(self):
csv_file = None
while not csv_file:
csv_file = self.download_csv(self.page_number)
self.queue.put((self.page_number, csv_file))
def download_csv(self, page_number):
print('download csv data [page=%s]' % page_number)
url = "http://api.intrinio.com/prices.csv?ticker=AAPL&hide_paging=true&page_size=100&page_number=%s" % page_number
auth = b'Basic ' + base64.b64encode(b'%s:%s' % (USERNAME, PASSWORD))
headers = {'Authorization': auth}
response = requests.get(url, headers=headers)
if response.ok:
return StringIO(response.text)
class ConvertThread(Thread):
'''
需要和打包线程通知状态
'''
def __init__(self, queue, c_event, t_event):
super().__init__()
self.queue = queue
self.c_event = c_event
self.t_event = t_event
def run(self):
count = 0
while True:
page_number, csv_file = self.queue.get()
if page_number == -1:
self.c_event.set()
self.t_event.wait()
break
self.csv_to_xml(csv_file, 'data%s.xml' % page_number)
count += 1
if count == 2:
count = 0
self.c_event.set()
self.t_event.wait()
self.t_event.clear()
def csv_to_xml(self, csv_file, xml_path):
print('Convert csv data to %s' % xml_path)
reader = csv.reader(csv_file)
headers = next(reader)
root = Element('Data')
root.text = '\n\t'
root.tail = '\n'
for row in reader:
book = SubElement(root, 'Row')
book.text = '\n\t\t'
book.tail = '\n\t'
for tag, text in zip(headers, row):
e = SubElement(book, tag)
e.text = text
e.tail = '\n\t\t'
e.tail = '\n\t'
ElementTree(root).write(xml_path, encoding='utf8')
class TarThread(Thread):
'''
打包线程,等待来自转换线程的通知
'''
def __init__(self, c_event, t_event):
'''
:param c_event: 转换线程事件
:param t_event: 打包线程事件
'''
super().__init__(daemon=True)
self.count = 0
self.c_event = c_event
self.t_event = t_event
def run(self):
while True:
self.c_event.wait()
self.c_event.clear()
print('打包......')
self.tar_xml()
self.t_event.set()
def tar_xml(self):
self.count += 1
tfname = 'data%s.tgz' % self.count
print('tar %s...' % tfname)
tf = tarfile.open(tfname, 'w:gz')
for fname in os.listdir('.'):
if fname.endswith('.xml'):
tf.add(fname)
os.remove(fname)
tf.close()
if not tf.members:
os.remove(tfname)
from threading import Event
if __name__ == '__main__':
queue = Queue()
c_event = Event()
t_event = Event()
thread_list = []
for i in range(1, 15):
t = DownloadThread(i, queue)
t.start()
thread_list.append(t)
convert_thread = ConvertThread(queue, c_event, t_event)
convert_thread.start()
tar_thread = TarThread(c_event, t_event)
tar_thread.start()
for t in thread_list:
t.join()
queue.put((-1, None))
convert_thread.join()
print('main thread end.')
- 转换和打包共享两个
Event
,转换结束使用set
解除打包中的wait
,打包结束解除转换中的wait
- 思考:
join
的作用,上面使用put(-1)
的方法,退出转换线程是否合理?
- 合理,join是阻塞,子线程完毕才会执行后面的代码!所以主线程中的
start()
一定要放在最前面
- 思考:一直在使用
io
包中的StringIO
,特点是什么?
- 思考:除了消息队列,操作系统还用什么实现进程、线程间通信?
问题:如何使用线程本地数据
- 场景:使用
opencv
库实时监控,我们在本地使用http
在浏览器获取视频数据
- 这里使用操作系统级别的管道
os.pipe()
发送摄像头数据给线程import os, cv2, time, struct, threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import TCPServer, ThreadingTCPServer
from threading import Thread, RLock
from select import select
class JpegStreamer(Thread):
def __init__(self, camera):
super().__init__()
self.cap = cv2.VideoCapture(camera)
self.lock = RLock()
self.pipes = {}
def register(self):
pr, pw = os.pipe()
self.lock.acquire()
self.pipes[pr] = pw
self.lock.release()
return pr
def unregister(self, pr):
self.lock.acquire()
pw = self.pipes.pop(pr)
self.lock.release()
os.close(pr)
os.close(pw)
def capture(self):
cap = self.cap
while cap.isOpened():
ret, frame = cap.read()
if ret:
ret, data = cv2.imencode('.jpg', frame, (cv2.IMWRITE_JPEG_QUALITY, 40))
yield data.tostring()
def send_frame(self, frame):
n = struct.pack('l', len(frame))
self.lock.acquire()
if len(self.pipes):
_, pipes, _ = select([], self.pipes.values(), [], 1)
for pipe in pipes:
os.write(pipe, n)
os.write(pipe, frame)
self.lock.release()
def run(self):
for frame in self.capture():
self.send_frame(frame)
class JpegRetriever:
def __init__(self, streamer):
self.streamer = streamer
self.local = threading.local()
def retrieve(self):
while True:
ns = os.read(self.local.pipe, 8)
n = struct.unpack('l', ns)[0]
data = os.read(self.local.pipe, n)
yield data
def __enter__(self):
if hasattr(self.local, 'pipe'):
raise RuntimeError()
self.local.pipe = streamer.register()
return self.retrieve()
def __exit__(self, *args):
self.streamer.unregister(self.local.pipe)
del self.local.pipe
return True
class WebHandler(BaseHTTPRequestHandler):
retriever = None
@staticmethod
def set_retriever(retriever):
WebHandler.retriever = retriever
def do_GET(self):
if self.retriever is None:
raise RuntimeError('no retriver')
if self.path != '/':
return
self.send_response(200)
self.send_header('Content-type', 'multipart/x-mixed-replace;boundary=jpeg_frame')
self.end_headers()
with self.retriever as frames:
for frame in frames:
self.send_frame(frame)
def send_frame(self, frame):
sh = b'--jpeg_frame\r\n'
sh += b'Content-Type: image/jpeg\r\n'
sh += b'Content-Length: %d\r\n\r\n' % len(frame)
self.wfile.write(sh)
self.wfile.write(frame)
from concurrent.futures import ThreadPoolExecutor
class ThreadingPoolTCPServer(ThreadingTCPServer):
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, thread_n=100):
super().__init__(server_address, RequestHandlerClass, bind_and_activate=True)
self.executor = ThreadPoolExecutor(thread_n)
def process_request(self, request, client_address):
self.executor.submit(self.process_request_thread, request, client_address)
if __name__ == '__main__':
streamer = JpegStreamer(0)
streamer.start()
retriever = JpegRetriever(streamer)
WebHandler.set_retriever(retriever)
HOST = 'localhost'
PORT = 9000
print('Start server... (http://%s:%s)' % (HOST, PORT))
httpd = ThreadingPoolTCPServer((HOST, PORT), WebHandler, thread_n=3)
httpd.serve_forever()
- 这里有个问题:只有一个客户端能获取到数据,也就是说无法共享
- 我们可以在第一个类中通过建立一个管道的字典,发送图像帧给多个客户端
- 上面使用
threading.local
,使用线程本地数据实现JpegRetriever
类,单独建立管道;解释一下特点:from threading import Thread, local
lo = local()
lo.x = 1
def ch_local(local):
local.x = 2
print(local.x)
Thread(target=ch_local, args=(lo,)).start()
print(lo.x)
- 也就是说,函数中的
local.x
其实是调用时新创建的,属于这个线程的本地(私有)数据,和主线程独立
- 也是利用这个特性,多个
WebHandler
线程中,每个retriever
都需要在注册管道时使用local
变量,相当于创建独立的管道通信
问题:如何使用线程池
- 代码已经在上部分展示,使用线程池对连接请求做限制,避免资源耗尽!
- 使用线程池还能避免频繁创建消耗资源,演示一下:
import threading, time
from concurrent.futures import ThreadPoolExecutor
def func(a,b):
print(threading.current_thread().name,a ,b)
time.sleep(0.01)
return a+b
exe = ThreadPoolExecutor(3)
result = exe.map(func, range(1,4), range(2,5))
print(list(result))
- 我们直接将
ThreadingTCPServer
继承,追踪代码找到他需要的__init__
参数,调用父类构造器并创建线程池,使用线程池的submit/map
调用它的核心方法process_request
中的process_request_thread
- 以上也是改写代码的基本流程!
问题:如何使用多进程
- 想要处理CPU密集型任务,可以使用多进程,看两段多进程的演示代码
from multiprocessing import Process
import time
x = 0
def func():
global x
x += 1
time.sleep(1)
print('x:',x)
if __name__ == '__main__':
p = Process(target=func)
p.start()
p.join()
print('x:',x)
print('main process end...')
- 和多线程之间最大的不同是,不同进程之间使用不同的虚拟地址空间(不共享资源)
- 所以,这里的全局
x
结果不同
- 进程之间的通信可以使用管道,演示一下:
from multiprocessing import Queue, Pipe, Process
def func(c):
data = c.recv()
print(data)
c.send(data + 1)
if __name__ == '__main__':
c1, c2 = Pipe()
p1 = Process(target=func, args=(c2,)).start()
c1.send(99)
print(c1.recv())
- 看一段判断水仙花数的代码:
from threading import Thread
from multiprocessing import Process
from queue import Queue as Thread_Queue
from multiprocessing import Queue as Process_Queue
def is_armstrong(n):
a, t = [], n
while t:
a.append(t % 10)
t //= 10
k = len(a)
return sum(x ** k for x in a) == n
def find_armstrong(a, b, q=None):
res = [x for x in range(a, b) if is_armstrong(x)]
if q:
q.put(res)
return res
def find_by_thread(*ranges):
q = Thread_Queue()
workers = []
for r in ranges:
a, b = r
t = Thread(target=find_armstrong, args=(a, b, q))
t.start()
workers.append(t)
res = []
for _ in range(len(ranges)):
res.extend(q.get())
return res
def find_by_process(*ranges):
q = Process_Queue()
workers = []
for r in ranges:
a, b = r
t = Process(target=find_armstrong, args=(a, b, q))
t.start()
workers.append(t)
res = []
for _ in range(len(ranges)):
res.extend(q.get())
return res
if __name__ == '__main__':
import time
t0 = time.time()
res = find_by_process([10000000, 15000000], [15000000, 20000000],
[20000000, 25000000], [25000000, 30000000])
print(res)
print(time.time() - t0)
- 注意区别进程中的
Queue
- 可以看一下CPU,一下4个核就满了
- 思考:进程间队列和线程间队列的区别是什么?
装饰器
问题:如何使用装饰器
- 场景:想为多个函数添加统一的功能,又不想写多份一样的代码
- 用两个递归的例子来说明
def fib(n):
if n<=1:
return 1
return fib(n-1) + fib(n-2)
print(fib(6))
def climb(n):
'''
:param n: 还剩多少台阶
:return: n个台阶的爬法
'''
count = 0
if n==0:
count = 1
elif n>0:
count = climb(n-1)+climb(n-2)+climb(n-3)
return count
print(climb(4))
- 跟踪上面两个递归的过程会发现,有很多重复的计算,我们可以建立
cache
加速
- 如何一次实现给两个函数加cache?
def deco(func):
cache = {}
def wrap(*args):
res = cache.get(args)
if not res:
res = cache[args] = func(*args)
return res
return wrap
fib = deco(fib)
print(fib(6))
@deco
def fib(n):
if n<=1:
return 1
return fib(n-1) + fib(n-2)
@deco
def climb(n):
'''
:param n: 还剩多少台阶
:return: n个台阶的爬法
'''
count = 0
if n==0:
count = 1
elif n>0:
count = climb(n-1)+climb(n-2)+climb(n-3)
return count
- 装饰之后秒出结果,这就是算法的魅力!使用cache也是加速递归最常用的思想
问题:如何保存原函数的信息
- 函数中保存着一些必要的信息
print(climb.__name__)
print(climb.__doc__)
print(climb.__module__)
print(climb.__dict__)
print(climb.__defaults__)
print(climb.__annotations__)
- 使用装饰器后,返回的就是wrap函数的信息了,怎么整?
- 我们可以在装饰器中将wrap的信息替换为func的,
wraps
装饰器帮我们实现了
from functools import update_wrapper, wraps
def deco(func):
@wraps(func)
def wrap(*args):
'''
这是wrap的文档
:param args:
:return:
'''
return func()
return wrap
@deco
def func(a):
'''
func的文档
:param a:
:return:
'''
print(a)
print(func.__doc__)
问题:如何定义带参数的装饰器
- 场景:使用装饰器检测函数参数类型,不同函数参数长度、类型不一,这就需要给装饰器定义参数
- 这个需求也可以理解为:生产装饰器的装饰器!
- 实现类型的检查,可以通过绑定参数实现,用
inspect
,下面是例子:import inspect
def func1(a,b,c):
print(a,b,c)
func1_sig = inspect.signature(func1)
bind = func1_sig.bind(int,int,str)
print(bind.arguments)
- 可以将传递给装饰器的参数类型和函数调用时的参数值分别绑定到函数,校验!
def deco_wrap(*typet, **typed):
def deco(func):
func_sig = inspect.signature(func)
bind = func_sig.bind_partial(*typet, **typed).arguments
def wrap(*args, **kwargs):
for name, value in func_sig.bind(*args, **kwargs).arguments.items():
right_type = bind[name]
if right_type:
if not isinstance(value, right_type):
raise TypeError("not match, %s must be %s type"%(name, right_type))
return func(*args, **kwargs)
return wrap
return deco
@deco_wrap(int, str)
def func2(a,b):
print(a,b)
func2(1,2)
问题:如何实现属性可修改的函数装饰器
- 场景
- 统计被装饰函数的单次运行时间
- 如果时间大于timeout,就记录到log中
- 能动态的修改
timeout
参数
import random
import logging
import time
def deco_wrap(timeout):
def deco(func):
def wrap(*args):
t0 = time.time()
res = func(*args)
use = time.time() - t0
if use>timeout:
logging.warning("%s %s %s"%(func.__name__, use, timeout))
return res
def set_time(new_timeout):
nonlocal timeout
timeout = new_timeout
wrap.set_time = set_time
return wrap
return deco
@deco_wrap(1.0)
def func(i):
while random.randint(0,1):
time.sleep(0.5)
print('in func[%d]'%i)
if __name__ == '__main__':
for i in range(15):
func(i)
if i==5:
func.set_time(1.5)
- 重点是学习装饰器内部定义多个函数并返回调用的方法
- 在程序中如果不是面向对象式编程(class中用self,一样的道理),必须要用
global
参数,函数才能访问外部全局变量
问题:如何在类中定义装饰器
- 上一个问题中,我们需要在函数中使用外部的变量,能否通过类的属性实现变量的全局传递呢?
- 当然可以!道理是相同的,Demo如下:
import time
import logging
DEFAULT_FORMAT = '%(func_name)s -> %(call_time)s\t%(used_time)s\t%(call_n)s'
class CallInfo:
def __init__(self, log_path, format_=DEFAULT_FORMAT, on_off=True):
self.log = logging.getLogger(log_path)
self.log.addHandler(logging.FileHandler(log_path))
self.log.setLevel(logging.INFO)
self.format = format_
self.is_on = on_off
def info(self, func):
_call_n = 0
def wrap(*args, **kwargs):
func_name = func.__name__
call_time = time.strftime('%x %X', time.localtime())
t0 = time.time()
res = func(*args, **kwargs)
used_time = time.time() - t0
nonlocal _call_n
_call_n += 1
call_n = _call_n
if self.is_on:
self.log.info(self.format % locals())
return res
return wrap
def set_format(self, format_):
self.format = format_
def turn_on_off(self, on_off):
self.is_on = on_off
import random
ci1 = CallInfo('mylog1.log')
ci2 = CallInfo('mylog2.log')
@ci1.info
def f():
sleep_time = random.randint(0, 6) * 0.1
time.sleep(sleep_time)
@ci1.info
def g():
sleep_time = random.randint(0, 8) * 0.1
time.sleep(sleep_time)
@ci2.info
def h():
sleep_time = random.randint(0, 7) * 0.1
time.sleep(sleep_time)
for _ in range(30):
random.choice([f, g, h])()
ci1.set_format('%(func_name)s -> %(call_time)s\t%(call_n)s')
for _ in range(30):
random.choice([f, g])()
print(type(g))
- 把类的实例方法作为装饰器,在wrap函数中就可以持有实例对象,便于修改属性和拓展功能!
- 装饰一个函数就实例化一个对象吧,避免牵一发而动全身!
小结
- 至此,总结了大部分的实用编程技巧,还有很多需要在工作中改进学习的地方,逐步积累!