Python 高级特性介绍 - 迭代的99种姿势 与协程
引言
写这个笔记记录一下一点点收获
测试环境版本:
- Python 3.7.4 (default, Sep 28 2019, 16:39:19)
Python2老早就停止支持了 所以还是跟进py3吧- macOS Catalina 10.15.1
迭代方式
Python中一样可以使用for进行迭代
与C、Java等一众语言有区别的是
python中迭代更像是Java的逐元循环(foreach)
Java用法(下标迭代):
for (int i = 0; i < array.length; ++i) {
operation(array[i]);
}
可以看到 对于存在下标的Java数组而言
利用数组下标进行遍历更加符合直觉(数组就是一块连续的内存空间
加上下标
作为偏移量)
Java内部实现:数组变量int[] array作为数据存储在Java的栈
里
而数组本身在堆
中创建 其引用
被赋值给array
等价的python代码:
for i in range(len(list)):
operation(list[i])
问题来了 如果也想使用相似的下标方式该怎么办呢
python自带了enumerate
函数可以帮助我们实现:
等价的python代码:
for i, value in enumerate(list):
operation(value)
其实python自带的
dict
本身实现了这个操作:
python同时对key和value进行迭代
for key, value in dict.items():
operation(key, value)
下面就是抽象程度更高的循环了:
其不光可以用在带下标
的数据类型上
对于可迭代(Iterable)
的所有元素都可以这样操作
Java用法(逐元循环):
int[] array = new int[len];
for(int i : array) {
// 注意这里是深拷贝
// 如果对i做赋值等操作无意义
operation(i);
}
等价的python用法:
for i in list:
operation(i)
生成器
在python中 有时候会遇到创建容量很大的list的需求
假如list中每个元素都可以利用算法推出来 如:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
就可以利用
列表生成式(List Comprehensions)
来实现:
a = [x for x in range(10)]
假如想生成
1e10
个元素呢?
一方面会遇到内存容量不足的情况
还有如果我们访问过前几个元素便不需要这个list了
就会造成极大的内存浪费
这里从案例引入
生成器
的概念
有时候小白在学习python
很容易把列表的创建符号打错
如下
a = {1, 2, 1}
学过c和Java的程序员以为这样会创建一个数组
但是在python里这是一个set(集合)
其特点是无序且不重复
于是上面的等价代码如下:
a = {1, 2}
和直觉(Java程序员的)相违背
还有同学写成了这样的形式:
a = (1, 2)
这样就创建了一个tuple(元组)
其特点是元素不可修改
最后一种小白写成了这样:
a = (x for x in range(10))
乍一看和上面列表生成式很像
用它迭代试试呢:
for i in range(len(a)):
print(i)
Traceback (most recent call last):
File "", line 1, in
TypeError: object of type 'generator' has no len()
嗯哼?出现了意料之外的结果
所以我们到底创建了一个什么呢?
用type()
看一看:
<class 'generator'>
哦豁 这是个啥
generator(生成器)
?
查一下资料 好像这个就是我们需要的
这个东西就可以解决上面提到的问题
不必占用大量内存 还可以满足迭代需求
对生成器的迭代方式:
next(generator)
如果迭代完成 会获得
StopIteration
的异常
当然这样很不优雅
要知道生成器也是可迭代(Iterable)
的:
for i in generator:
operation(i)
这样就可以愉快的迭代了
等会 如果想生成一个无法用列表生成式表达的list呢?
比如 斐波那契数列
这样很容易利用函数写出 却无法使用一层for直接给出的
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
这个函数可以输出fib的前n个数
那么怎么得到这样的生成器呢?
很简单 只需要把输出语句改为yield即可:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
yield在其中的作用相当于return
其英文意思本身就是产出
嘛
但是是在每次调用next时return
下一次从yield处重新开始计算
这样就可以方便的迭代斐波那契数列了:
for i in fib(100):
print(i)
迭代器
迭代器(Iterator)和可迭代(Iterable)
python内置的集合数据类型都是可迭代的
如list
、tuple
、set
、dict
、str
等
生成器
也是可迭代
的
总而言之 可以作用于for循环的对象都是可迭代的
但是迭代器是另一个概念
代表可以被next()函数调用的对象
其一定是惰性计算的
集合数据类型如list
、dict
、str
等是可迭代但不是迭代器
不过可以通过iter()函数获得一个迭代器对象
迭代器通常表示一个数据流 并且可以是无限大的
在Java中 对
集合(Collections)
的遍历操作可以通过迭代器进行
迭代器的基本方法有hasNext()
、next()
、remove()
它支持以不同的方式遍历一个聚合对象
同时还有ListIterator
扩展Iterator接口
来实现列表的双向遍历和元素的修改
这一点也和设计模式中迭代器模式很相似
其优点有:
- 访问一个聚合对象的内容而无须暴露它的内部表示
- 需要为聚合对象提供多种遍历方式
- 为遍历不同的聚合结构提供一个统一的接口
其实Java的编译器会自动把标准的foreach循环自动转换为Iterator遍历
因为Iterator对象是集合对象自己在内部创建的
它自己知道如何高效遍历内部
的数据集合
python的协程
你以为这篇笔记到此为止了吗?
天真 其实才刚刚开始
这篇写作的动机在于
go的协程和python的协程
首先复习一下
进程、线程和协程的基本概念
进程:操作系统资源分配
的最小单位
线程:操作系统资源调度
的最小单位
协程:语言层面
实现的对线程的调度
程序:指令、数据及其组织形式的描述
进程:程序的实体
多线程:在单个程序中同时运行多个线程完成不同的工作
go的设计哲学
最重要的就有一个:
不要使用共享内存来通信 要使用通信来共享内存
现在都讲究高并发
挺重要的一点就是异步操作
比如io操作通常需要是异步的
这一点在前端的一些语言中体现的比较多
比如setTimeout()
比如微信小程序开发中
会用到promise回调
微信中常见的用到异步回调接口
wx.function({
success: () => console.log('success'),
fail: () => console.log('failure'),
})
这样很不优雅
因为一旦逻辑多了 小白很容易写成回调地狱形式
解决方案是可以封装成promise回调
这里直接贴一道面试题吧
调用async
修饰的方法会直接返回一个Promise
对象
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
})
console.log('script end')
实测的输出:
[Log] script start
[Log] async1 start
[Log] async2
[Log] promise1
[Log] script end
[Log] promise2
[Log] async1 end
< undefined
[Log] setTimeout
如果更追求优雅的话 封成proxy都可以
这里略过不表
python就借鉴了前端
async
和await
的模式
(其实这才是异步的终极解决方案
么
内置实现是asyncio
库
import一下 看看内部有什么实现
dir(asyncio)
['ALL_COMPLETED', 'AbstractChildWatcher', 'AbstractEventLoop', 'AbstractEventLoopPolicy', 'AbstractServer', 'BaseEventLoop', 'BaseProtocol', 'BaseTransport', 'BoundedSemaphore', 'BufferedProtocol', 'CancelledError', 'Condition', 'DatagramProtocol', 'DatagramTransport', 'DefaultEventLoopPolicy', 'Event', 'FIRST_COMPLETED', 'FIRST_EXCEPTION', 'FastChildWatcher', 'Future', 'Handle', 'IncompleteReadError', 'InvalidStateError', 'LifoQueue', 'LimitOverrunError', 'Lock', 'PriorityQueue', 'Protocol', 'Queue', 'QueueEmpty', 'QueueFull', 'ReadTransport', 'SafeChildWatcher', 'SelectorEventLoop', 'Semaphore', 'SendfileNotAvailableError', 'StreamReader', 'StreamReaderProtocol', 'StreamWriter', 'SubprocessProtocol', 'SubprocessTransport', 'Task', 'TimeoutError', 'TimerHandle', 'Transport', 'WriteTransport', 'all', 'builtins', 'cached', 'doc', 'file', 'loader', 'name', 'package', 'path', 'spec', '_all_tasks_compat', '_enter_task', '_get_running_loop', '_leave_task', '_register_task', '_set_running_loop', '_unregister_task', 'all_tasks', 'as_completed', 'base_events', 'base_futures', 'base_subprocess', 'base_tasks', 'constants', 'coroutine', 'coroutines', 'create_subprocess_exec', 'create_subprocess_shell', 'create_task', 'current_task', 'ensure_future', 'events', 'format_helpers', 'futures', 'gather', 'get_child_watcher', 'get_event_loop', 'get_event_loop_policy', 'get_running_loop', 'iscoroutine', 'iscoroutinefunction', 'isfuture', 'locks', 'log', 'new_event_loop', 'open_connection', 'open_unix_connection', 'protocols', 'queues', 'run', 'run_coroutine_threadsafe', 'runners', 'selector_events', 'set_child_watcher', 'set_event_loop', 'set_event_loop_policy', 'shield', 'sleep', 'sslproto', 'start_server', 'start_unix_server', 'streams', 'subprocess', 'sys', 'tasks', 'transports', 'unix_events', 'wait', 'wait_for', 'wrap_future']
可以看到 内部方法还是挺多的
介绍如下:
asyncio 提供一组高层级
API 用于:
并发
地运行Python 协程并对其执行过程实现完全控制- 执行
网络IO
和IPC
- 控制
子进程
- 通过
队列
实现分布式
任务 同步
并发代码
在这个库出现之前怎么写异步呢?
前面介绍了生成器
和yield
但是少介绍了一个函数
和next()
配套使用的send()
next()
完全等价于send(None)
子程序
就是协程
的一种特例
这里引用廖雪峰的一个生产者消费者模型:
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
可以看到整个流程
无锁
由一个线程
执行
produce和consumer协作
完成任务
所以称为“协程”
而非线程的抢占式多任务
这样写很不优雅
但是在asyncio出现之前的协程只有这一种写法
出现之后:
import asyncio
@asyncio.coroutine
def hello():
print("Hello world!")
# 异步调用asyncio.sleep(1):
r = yield from asyncio.sleep(1)
print("Hello again!")
# 获取EventLoop:
loop = asyncio.get_event_loop()
# 执行coroutine
loop.run_until_complete(hello())
loop.close()
熟悉前端编程的同学应该看出来了
@asyncio.coroutine
这不就是async
yield from
这不就是await
么
有个小坑就是
原生的生成器不能直接用于await操作
需要用async修饰之后
生成器变成了异步生成器
这样就可以作用于await操作了
这一点go和erlang等语言做的就很好
python因为是后来才支持的协程
所以如果一个方法是async的
连带着调用的所有方法都需要是async的
python内部实现是
eventloop模型
go和erlang的实现是CSP(Communicating Sequential Processes)
这里略过不表
import asyncio
# @asyncio.coroutine
async def hello():
print('hello')
# r = yield from asyncio.sleep(5)
r = await asyncio.sleep(5)
print('hello again '.format(r))
# @asyncio.coroutine
async def wget(host):
print('wget host:{}'.format(host))
conn = asyncio.open_connection(host, 80)
# reader, writer = yield from conn
reader, writer = await conn
header = 'GET / HTTP/1.0
Host: {}
'.format(host)
writer.write(header.encode('utf-8'))
# yield from writer.drain()
await writer.drain()
while True:
# line = yield from reader.readline()
line = await reader.readline()
if line == b'
':
break
print('{} header: {}'.format(host, line.decode('utf-8').rstrip()))
writer.close()
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
这一点在后端编程上用的比较多
补充知识点就是各种io模型
比如BIO、NIO、AIO等
一般Java面试会碰到吧
而go的协程作为其最大的特性之一
一开始就占据了先机
内部实现是用的goroutine
和channel
通信机制非常简单
传数据用channel <- data
取数据用<-channel
go的
MPG
模型:
M
即Machine
一个M
直接关联一个内核线程
P
即Processor
代表M
需要的上下文
环境 也是处理用户级代码逻辑
的处理器
G
即Goroutine
本质上也是一种轻量级的线程
go采用的这种模型 从语言层面支持了并发
其实现挺像Java的线程池的 但是轻轻松松创建百万个goroutine
Java线程创几万个就会占用很高的内存了(大概1个1MB左右)
参考
廖雪峰的python教程
谷歌来的各种资料
后面应该会再写一个关于装饰器的文章吧
大概(咕咕咕