念念不忘,必有回响

Python 高级特性介绍 - 迭代的99种姿势 与协程

Python 高级特性介绍 - 迭代的99种姿势 与协程

引言

写这个笔记记录一下一点点收获
测试环境版本:

  1. Python 3.7.4 (default, Sep 28 2019, 16:39:19)
    Python2老早就停止支持了 所以还是跟进py3吧
  2. 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内置的集合数据类型都是可迭代的
listtuplesetdictstr
生成器也是可迭代
总而言之 可以作用于for循环的对象都是可迭代的

但是迭代器是另一个概念
代表可以被next()函数调用的对象
其一定是惰性计算的
集合数据类型如listdictstr等是可迭代但不是迭代器
不过可以通过iter()函数获得一个迭代器对象
迭代器通常表示一个数据流 并且可以是无限大的

在Java中 对集合(Collections)的遍历操作可以通过迭代器进行
迭代器的基本方法有hasNext()next()remove()
它支持以不同的方式遍历一个聚合对象
同时还有ListIterator扩展Iterator接口来实现列表的双向遍历和元素的修改
这一点也和设计模式中迭代器模式很相似

其优点有:

  1. 访问一个聚合对象的内容而无须暴露它的内部表示
  2. 需要为聚合对象提供多种遍历方式
  3. 为遍历不同的聚合结构提供一个统一的接口

其实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就借鉴了前端asyncawait的模式
(其实这才是异步的终极解决方案
内置实现是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 用于:

  1. 并发地运行Python 协程并对其执行过程实现完全控制
  2. 执行网络IOIPC
  3. 控制子进程
  4. 通过队列实现分布式任务
  5. 同步并发代码

在这个库出现之前怎么写异步呢?
前面介绍了生成器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\r\nHost: {}\r\n\r\n'.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'\r\n':
            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的协程作为其最大的特性之一
一开始就占据了先机
内部实现是用的goroutinechannel
通信机制非常简单
传数据用channel <- data
取数据用<-channel

go的MPG模型:
MMachine 一个M直接关联一个内核线程
PProcessor 代表M需要的上下文环境 也是处理用户级代码逻辑的处理器
GGoroutine 本质上也是一种轻量级的线程

go采用的这种模型 从语言层面支持了并发
其实现挺像Java的线程池的 但是轻轻松松创建百万个goroutine
Java线程创几万个就会占用很高的内存了(大概1个1MB左右)

参考

廖雪峰的python教程
谷歌来的各种资料
后面应该会再写一个关于装饰器的文章吧
大概(咕咕咕

posted on 2020-01-27 05:41  licsber  阅读(602)  评论(0编辑  收藏  举报