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教程

谷歌来的各种资料

后面应该会再写一个关于装饰器的文章吧

大概(咕咕咕

最新文章

  1. java spring mvc restful 上传文件
  2. 掌握Thinkphp3.2.0----CURD
  3. Socket开发框架之消息的回调处理
  4. 数据结构作业——sights(最短路/最近公共祖先)
  5. POJ 2891 Strange Way to Express Integers(拓展欧几里得)
  6. Hadoop之 hdfs 系统
  7. 从零开始攻略PHP(9)——错误和异常处理
  8. http://my.oschina.net/u/719192/blog/506062?p={{page}}
  9. Bundle、Intent、SharedPreferences
  10. BW知识点总结及面试要点
  11. poj 2186 强连通分支 和 spfa
  12. CSS 去除浏览器默认 轮廓外框
  13. (转)swfobject.js 详细解说
  14. UWP开发的一些思考
  15. 视频编辑类sdk--lansoeditor--更新啦, 完全免费,欢迎下载
  16. CocoaPods配置步骤
  17. 将float转换为数据类型numeric时出现算术溢出错误
  18. 2010-2011 ACM-ICPC, NEERC, Moscow Subregional Contest Problem H. Hometask 水题
  19. mysql Substr与char_length函数的应用
  20. 在eclipse中查看sources源码和JavaDoc帮助文档

热门文章

  1. AttributeError: &#39;SQLAlchemy&#39; object has no attribute &#39;Foreignkey&#39;
  2. Web安全测试学习笔记 - 文件包含
  3. 给阿里云主机添加swap分区,解决问题:c++: internal compiler error: Killed (program cc1plus)
  4. ios 软键盘弹出布局被顶上去 已解决
  5. Java接收前台传回的json
  6. mybatis(六):设计模式 - 组合模式
  7. 图的最短路径算法Dijkstra算法模板
  8. 转载:DRC
  9. Linux新建用户,切换后只显示$问题
  10. SAIF anno