我们在前面应该写过类似的代码

for i in [1,2,3,4,5]:
print(i)

for in 语句看起来很直观,很便于理解,比起C++或Java早起的

for (int i = ; i<n;i++)
printf("d\n",a[i])

是不是简洁清晰的多。但是我们有没有想过Python在处理for in语句的时候,具体发生了什么吗?什么样的对象可以被for in用来枚举呢?

所以,这一节我们就深入到Python的容器类型实现底层看一看,了解一下迭代器和生成器。

前面用过的容器、可迭代对象和迭代器

容器这个概念还是比较容易理解的,我们说过,Python中一切皆为对象,对象的抽象就是类,而对象的集合就是容器。

l = [0,1,2]
t = (0,1,2)
d = {0:0,1:1,2:2}
s = set([0,1,2])

上面的列表,元组、字典和集合都是容器。对于容器,我们可以很直观的想象成很多个元素在一起的单元;而不同容器的区别,正式在于内部数据结构的实现方法。然后我们就可以针对不同的场景,选择不同时间复杂度和空间复杂度的容器。

所有的容器都是可迭代的(iterable)。这里的迭代,和枚举是完全不同的,这里的迭代可以想象成去买个苹果,卖家并声明他有多少库存,这样,每次去买一个苹果的时候卖家采取的行为不外乎给你拿一个苹果,要么就是告诉你苹果已经卖完了,所以你并不需要知道卖家是怎么在仓库内存放苹果的。

严谨 都说,迭代器(iterator)提供了一个next的方法,调用这个方法后,要么叨叨容器的下一个对象,要么得到一个StopIteration的错误。我们并不需要想列表一样指定元素的索引,因为字典和集合是没有索引的说法的(字典采用哈希表实现)。我们只需要知道,next函数可以不重复不遗漏的拿到所有的元素就可以了。

而可迭代对象,是通过iter()函数返回一个迭代器,在通过next()函数就可以实现遍历。for in语句将这个过程隐式化,我们只需要大概知道他怎么做就行了。

我们再看看下面的代码,主要展示了如何判断一个对象是否可迭代。当然还有一种用法,是isinstance(obj,Iterable)。

def is_iterable(param):
try:
iter(param)
return True
except TypeError:
return False params = [
'',
1234,
[1,2,3,4],
set([1,2,3,4]),
{1:1,2:2,3:3},
(1,2,3,4)
] for param in params:
print('{} is iterable?{}.'.format(param,is_iterable(param)))
1234 is iterable?True.
1234 is iterable?False.
[1, 2, 3, 4] is iterable?True.
{1, 2, 3, 4} is iterable?True.
{1: 1, 2: 2, 3: 3} is iterable?True.
(1, 2, 3, 4) is iterable?True.

输出

通过上面的代码可以发现,在给出的类型中,只有数字1234是不可迭代的,其余数据类型都可以迭代。

what is 生成器?

那么生成器又是什么呢?在很多语言中,生成器都没有相对应的模型,所以这里只需要记住一点:生成器就是懒人版的迭代器。

如果想要在迭代器中枚举他的元素,这些元素要事先生成。这里,我们看看下面的例子

import os
import psutil def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid) info = p.memory_full_info()
memory = info.uss / 1024. /1024
print('{} memory used:{}MB'.format(hint,memory)) def test_iterator():
show_memory_info('initing iterator')
list_1 = [i for i in range(100000000)]
show_memory_info('after iterator initiated')
print(sum(list_1)) def test_generator():
show_memory_info('intiting generator')
list_2 = (i for i in range(100000000))
show_memory_info('after generator initiated')
print(sum(list_2))
show_memory_info('after sum called') test_iterator()
test_generator()
initing iterator memory used:7.21875MB
after iterator initiated memory used:1848.28515625MB
4999999950000000
intiting generator memory used:1.7109375MB
after generator initiated memory used:1.7421875MB
4999999950000000
after sum called memory used:2.109375MB

输出


我们用[i for i in range(100000000)]声明了一个包含一个亿元素的列表(声明了一个迭代器),每个元素在生成以后都保存在内存里,通过代码可以发现他们占用了巨大的内存空间,如果内存不够的话就直接OOM错误了。

不过,我们并不需要在内存中同事保存这么多东西,比方元素求和,我们只需要知道每个元素相加的那一刻是多少就可以了,用完扔掉即可。

于是,生成器在这里就体现出作用了。在我们调用next()函数的时候,才会生成下一个变量,生成器在Python中的写法是用小括号括起来:

l = (i for i in range(100000000))

这样一来,可以清晰的看到生成器是不会像迭代器一样占用大量内存的,只有在被使用的时候才会调用,而且生成器在初始化的时候,并不需要一次生成操作,相比于例子中的第一个测试函数,第二个函数节省了一次生成一亿个元素的过程,因此耗时明显变短。

此外,生成器并不是单单节省了时间和计算机资源,我们可以看看下面的例子。

生成器还有什么作用?

数学中有一个恒等式:

(1+2+3+...+n)^2 = 1^3+2^3+3^3+...+n^3

如果我们想验证一下他,要怎么码代码呢?

def generator(k):
i = 1
while True:
yield i**k
i += 1 gen_1 = generator(1)
gen_3 = generator(3) print(gen_1)
print(gen_3) def get_sum(n):
sum_1 ,sum_3 = 0,0
for i in range(n):
next_1 = next(gen_1)
next_3 = next(gen_3)
print('next_1 = {},next_3 = {}'.format(next_1,next_3))
sum_1 += next_1
sum_3 += next_3
print(sum_1*sum_1,sum_3) get_sum(10)

首先,可以注意一下generator()这个函数,他返回了一个生成器。

下面的yield可以说是程序的关键,因为函数运行到这里的时候,是会暂停的,然后跳出到next()函数处。而i**k则成了next()函数的返回值。

这样,每次next(gen)函数被调用的时候,暂停的程序就重新复活,从yield处向下继续执行,同时要注意的是,局部变量i并没有被清除掉,而是会继续累加,所以next_1和next_3是不停变化的。

所以说,这个生成器是可以无限制的一直进行下去。迭代器是一个有限的集合,而生成器则可以视为一个无限集合,我们只管调用next()就可以,生成器根据运算会自动生成新的元素返回,非常方便。

我们看看下面这段代码

def index_normal(L,target):
result = []
for i,num in enumerate(L):
if num == target:
result.append(i)
return result print(index_normal([1,2,3,4,5,6,7,8,2],2))

就是获取列表中和指定元素相同的索引值组成的列表,那我们用下面的方法是不是简单的多?

def index_generator(L,target):
for i,num in enumerate(L):
if num == target:
yield i print(list(index_generator([1,2,3,4,5,6,7,8,2],2)))

到这里就不用多做解释了,唯一需要强调的是index_generator会返回一个Generator对象,需要用list转换为列表后才能打印。

这里有个事情要强调:

在Python的语言规范中,用更少,更加清晰的代码实现相同的功能一直是被推崇的办法。因为这样能够很有效的提高代码的可读性,减少出错概率,也能方便他人快速准确的理解作者的意图。但是,这里的“更少”的前提是清晰,而非使用更多的魔术操作,即便减少了代码反而增加了阅读的难度。

回归正题,我们看一看这样的问题,给定两个序列,判定第一个序列是不是第二个序列的子序列。(子序列,一个列表的元素在第二个列表中按顺序出现,注意按顺序,比方[1,3,5]是[1,2,3,4,5]的子序列,而[1,5,3]就不是)

a = ['a','c','d']
b = ['a','b','c','d','e'] def is_subsequence(list_1,list_2):
if not list_1:
return True
else:
for x in list_1:
if not (x in list_2):
return False
else:
if is_subsequence(list_1[list_1.index(x)+1:],list_2[list_2.index(x)+1:]):
return 'list_1 is subsequence of list_2'
else:
return 'list_1 is not subsequence of list_2'
print(is_subsequence(a,b))

上面的代码就是用了叫做‘贪心算法’的常规算法,我们维护两个指针指向两个列表的最开始,然后对第二个列表一路扫过去,如果某个数字和第一个指针指的一样,那么就把第一个指针前进一步。直到一个指针移出第一个序列。

那么我们要用生成器和迭代器的方法该怎么实现呢?

我们先看一个极简版的

a = ['a','c','d']
b = ['a','b','c','d','e']
a1 = ['a','d','c'] def is_subsequence(a,b):
b = iter(b)
return all(i in b for i in a)
print(is_subsequence(a,b))
print(is_subsequence(a1,b))

看完是不是感觉一脸蒙逼?没关系,我们把他复杂化,一步一步看

a = ['a','c','d']
b = ['a','b','c','d','e']
def is_subsequence(a,b):
b = iter(b)
print(b) gen = (i for i in a)
print(gen) for i in gen:
print(i) gen = ((i in b)for i in a)
print(gen) return all(((i in b)for i in a))
print(is_subsequence(a,b))

在函数开始,先把列表b转换成了迭代器,用途后面再讲

接下来的gen=。。。比较好理解,就是产生一个生成器,这个生成器用来变量列表a,所以可以输出a里的数据。而i in b就需要好好理解了,这里是不是能联想到 for in 语句?

没错,这里的i in b大概可以等价于下面的代码

while True:
val = next(b)
if val == i:
yield True

所以这里就非常巧妙的利用都了生成器的特性,next()函数运行的时候,保存了当前的指针,再看看下面的示例:

b = (i for i in range(5))
print(2 in b)
print(6 in b) ####输出####
True
False

最后的all()函数就很简单了,他用来判断一个迭代器的元素是否全为True,如果是则返回True,否则就返回False。

总结

容器是可迭代对象,可迭代对象调用iter()函数,可以得到一个迭代器,迭代器可以通过next()获得下一个元素从而支持遍历。

生成器是一种特殊的迭代器(注意反向逻辑是不成立的)。使用生成器,可以写出来更加清晰的代码,合理使用生成器可以有效降低内存使用、优化程序结构、提高程序速度。

生成器在Python2的版本上是协程的一种重要实现方式,而在3.5引入asyncawait语法糖后,生成器实现协程的方式就已经落后了。

课后思考

对于一个有限元素的生成器,如果迭代完成后,继续调用next()会发生什么呢?生成器可以遍历多次么?

Python核心技术与实战——十五|深入了解迭代器和生成器的更多相关文章

  1. Python核心技术与实战——十九|一起看看Python全局解释器锁GIL

    我们在前面的几节课里讲了Python的并发编程的特性,也了解了多线程编程.事实上,Python的多线程有一个非常重要的话题——GIL(Global Interpreter Lock).我们今天就来讲一 ...

  2. Python核心技术与实战——十六|Python协程

    我们在上一章将生成器的时候最后写了,在Python2中生成器还扮演了一个重要的角色——实现Python的协程.那什么是协程呢? 协程 协程是实现并发编程的一种方式.提到并发,肯很多人都会想到多线程/多 ...

  3. Python核心技术与实战——十|面向对象的案例分析

    今天通过面向对象来对照一个案例分析一下,主要模拟敏捷开发过程中的迭代开发流程,巩固面向对象的程序设计思想. 我们从一个最简单的搜索做起,一步步的对其进行优化,首先我们要知道一个搜索引擎的构造:搜索器. ...

  4. Python核心技术与实战——十八|Python并发编程之Asyncio

    我们在上一章学习了Python并发编程的一种实现方法——多线程.今天,我们趁热打铁,看看Python并发编程的另一种实现方式——Asyncio.和前面协程的那章不太一样,这节课我们更加注重原理的理解. ...

  5. Python核心技术与实战——十四|Python中装饰器的使用

    我在以前的帖子里讲了装饰器的用法,这里我们来具体讲一讲Python中的装饰器,这里,我们从前面讲的函数,闭包为切入点,引出装饰器的概念.表达和基本使用方法.其次,我们结合一些实际工程中的例子,以便能再 ...

  6. Python核心技术与实战——十二|Python的比较与拷贝

    我们在前面已经接触到了很多Python对象比较的例子,例如这样的 a = b = a == b 或者是将一个对象进行拷贝 l1 = [,,,,] l2 = l1 l3 = list(l1) 那么现在试 ...

  7. python自动华 (十五)

    Python自动化 [第十五篇]:CSS.JavaScript 和 Dom介绍 本节内容 CSS javascript dom CSS position标签 fixed: 固定在页面的某个位置 rel ...

  8. 流畅的python 14章可迭代的对象、迭代器 和生成器

    可迭代的对象.迭代器和生成器 迭代是数据处理的基石.扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项.这就是迭代器模式(Iterator pattern). 迭 ...

  9. Python核心技术与实战——六|异常处理

    和其他语言一样,Python中的异常处理是很重要的机制和代码规范. 一.错误与异常 通常来说程序中的错误分为两种,一种是语法错误,另一种是异常.首先要了解错误和异常的区别和联系. 语法错误比较容易理解 ...

随机推荐

  1. TimePicker 时间选择器

    用于选择或输入日期 固定时间点 提供几个固定的时间点供用户选择 使用 el-time-select 标签,分别通过star.end和step指定可选的起始时间.结束时间和步长 <el-time- ...

  2. bulk_create(lst) 批量创建数据

    # 批量创建数据 # Create your views here. from django.db import models from django.shortcuts import HttpRes ...

  3. 网易云课堂_C++程序设计入门(下)_期末考试_期末考试在线编程题目

    期末考试在线编程题目 返回考试   本次考试题目一共两个,在考试期间可以不限制次数地提交 温馨提示: 1.本次考试属于Online Judge题目,提交后由系统即时判分. 2.学生可以在考试截止时间 ...

  4. C# 虚拟键盘

    [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint ...

  5. Function Expression

    One of the key characteristics of function declarations is function declaration hoisting, whereby fu ...

  6. [转] 浅谈JS中的变量及作用域

    Situation One <script> var i; function sayHello() { var x=100; alert(x); x++; } sayHello();   ...

  7. 求第n个质数

    输入一个不超过 10000 的正整数 n,求第n个质数 样例输入 10 样例输出 29 题目地址 #include<stdio.h> #include<math.h> int ...

  8. Java中volatile关键字的最全总结

    转载至:https://blog.csdn.net/u012723673/article/details/80682208 关于volatile很重要的一点: 它保证了可见性,即其他线程对volati ...

  9. 【神经网络与深度学习】Caffe使用step by step:caffe框架下的基本操作和分析

    caffe虽然已经安装了快一个月了,但是caffe使用进展比较缓慢,果然如刘老师说的那样,搭建起来caffe框架环境比较简单,但是完整的从数据准备->模型训练->调参数->合理结果需 ...

  10. 【VS开发】【智能语音处理】解读男女声音的区别:亮度,糙度

    1. 男女声音的本质不同:音高不同 这是废话,地球人都知道.都说女声比男声高八度,其实不能,高4-6度差不多. 2. 男女声音的不同:亮度 从直观上这个很好理解,女声普遍更"亮", ...