专栏首页王的机器盘一盘 Python 系列特别篇 - 两大利「器」

盘一盘 Python 系列特别篇 - 两大利「器」

引言

本文作为 Python 系列的特别篇第 3 篇,主要介绍 Python 里的两大利「器」,生成器 (generator) 和迭代器 (iterator)。

抱歉两大利器这个标题有点标题党,因为此器非彼器,我只能用干货内容来弥补。接下来 2 节来详细介绍这两大利器。

1.生成器

定义生成器 (generator) 有两种方法:

  1. 使用函数 (function)
  2. 使用表达式 (expression)

1.1第一种方法

首先看一个简单函数 square,计算列表里每个数的平方值。

def square(nums):    results = []    for num in nums:        results.append(num*num)    return results

输入 [1, 2, 3, 4, 5],输出 [1, 4, 9, 16, 25]

my_nums = square([1, 2, 3, 4, 5])print(my_nums)
[1, 4, 9, 16, 25]

在 square 函数里面做点两个小修改:

  1. 不要 return (即不需要定义列表 results)
  2. 使用 yield (给定一个 num 来 yield 一个 num*num)
def square(nums):    for num in nums:        yield num*num

这时 square 不再是函数了,而是一个生成器了。因此记住,用关键词 return 的是函数,用关键词 yield 的是生成器。打印来看看。

my_nums = square([1, 2, 3, 4, 5])print(my_nums)
<generator object square at 0x00000179D3629ED0>

和函数 square 的输出相比,生成器 square 的输出不是一个列表,而是一段乏味的信息。

如何来看生成器里的元素呢?有两种方法:1. 转换成 list;2. 用 next()。

转换成 list

list(my_nums)
[1, 4, 9, 16, 25]

虽然打印出了结果,但这不是生成器的用法。这样做的话还不如直接用列表解析式呢。

生成器中真正有特点的用法是用 next() 把不断获得下一个返回值

用 next()

先打印出一个元素。

print(next(my_nums))
1

这时候生成器内部状态 (state) 已经更新到第 2 个元素了,即 [1, 4, 9, 16, 25] 里面的 4。接着打印四遍 next()。

print(next(my_nums))print(next(my_nums))print(next(my_nums))print(next(my_nums))
4
9
16
25

这时候生成器内部状态已经更新到第 5 个元素,即最后一个元素了,再用 next() 会发生什么呢?

print(next(my_nums))

果然,报错了!StopIteration 意思就是迭代 (iteration) 停止 (stop) 了。等等,迭代?生成器可以被迭代?那生成器可不可以叫做迭代器?可以的!至于迭代器是第 2 节的内容。

我们知道 for 循环就是遍历一个迭代器里的每个元素的,那试试用 for 循环来遍历生成器 my_nums。

my_nums = square([1, 2, 3, 4, 5])
for num in my_nums:    print(num)
1
4
9
16
25

总结:生成器可以用生成函数 (generator function) 来定义,记住要用 yield 而不是 return。

1.2第二种方法

复习〖Python 入门篇 (下)〗第 5 节回忆一下列表解析式。注意用中括号 [] 来定义列表解析式。

my_nums = [x*x for x in [1,2,3,4,5]]print(my_nums)
[1, 4, 9, 16, 25]

要定义生成器,只需要把中括号 [] 换成小括号 ()。

my_nums = (x*x for x in [1,2,3,4,5])print(my_nums)
<generator object <genexpr> at 0x00000179D3C4B048>
print(list(my_nums))
[1, 4, 9, 16, 25]

总结:生成器可以用生成表达式 (generator expression) 来定义,记住和列表解析式很像,将 [] 改成 () 即可。

1.3生成器 vs 列表

两种用函数和表达式来定义生成器的方法介绍完了,现在思考,生成器好在哪里?好就好在生成器是按需求调用 (call-by-need) 的,你需要调用一个值,我就 yield 一个值,然后用 next() 更新内部状态,等待你下次调用。这套流程也称作惰性求值 (lazy evaluation),目的是最小化计算机要做的工作。

在大规模数据时,一次性处理往往抵消而且不方便,而惰性求值解决了这个问题,它把计算的具体步骤延迟到了要实际用该数据的时候。

接下来我们做个小实验,对比一下生成器和列表在运行简单操作时的用的时间占的内存

首先引入记录运行时间的 time 和占内存大小的 sys。

import timeimport sys

做 1 千万次 x+1 的操作,生成列表 l 和生成器 g。

%time l = [x+1 for x in range(10000000)]print(sys.getsizeof(l))
%time g = (x+1 for x in range(10000000))print(sys.getsizeof(g))
Wall time: 1.28 s
81528056

Wall time: 0 ns
120

比较两者用时和占用内存,谁优谁劣一目了然。

2.迭代器

在介绍迭代器 (Iterator) 之前,先介绍可迭代对象 (Iterable)。

2.1

可迭代对象

任何只要可以循环的东西就可称之可迭代对象 (iterable)。容器类型数据 (str, tuple, list, dict, set) 都可以被 for 循环,因此它们都是可迭代对象

判断方法一

在 Python 里万物皆对象,如果真要判断一个对象是否是可迭代对象,我们可以用 isinstance(x, Iterable)。

from collections import Iterable
print(isinstance([1,2,3], Iterable))    # listprint(isinstance({'1':23}, Iterable))   # dictprint(isinstance((1,2,3), Iterable))    # tupleprint(isinstance({1,2,3}, Iterable))    # setprint(isinstance('123', Iterable))      # strprint(isinstance(123, Iterable))        # intprint(isinstance(123.0, Iterable))      # float
True
True
True
True
True
False
False

结果正常,str, tuple, list, dict, set 都是可迭代对象,而 int 和 float 不是。

判断方法二

在 Python 里万物皆对象,那么我们可以查看该对象对应的类里面的属性,用 dir() 函数。里面有 __iter__() 魔法方法 (magic method) 的对象就是可迭代对象。不清楚上面这些知识可参考〖Python 特别篇 - 面向对象编程〗一贴。

列表 [1, 2, 3] 里有 __iter__() 魔法方法,它是可迭代对象

print(dir([1,2,3]))

整数 123 里没有 __iter__() 魔法方法,它不是可迭代对象

print(dir(123))

2.2迭代器

可被 for 循环的列表、字典、元组、集合和字符串都是可迭代对象,但实际上 for 循环里真正的对象是迭代器

首先用 isinstance(x, Iterator) 来判断它们 5 个是不是迭代器

from collections import Iterator
print(isinstance([1,2,3], Iterator))    # listprint(isinstance({'1':23}, Iterator))   # dictprint(isinstance((1,2,3), Iterator))    # tupleprint(isinstance({1,2,3}, Iterator))    # setprint(isinstance('123', Iterator))      # str
False
False
False
False
False

都不是的!那它们怎么能被 for 循环呢?原来 for 循环先用 __iter__() 方法将它们都转成迭代器,再开始遍历它们的每个元素。

print(isinstance(iter([1,2,3]), Iterator))    # listprint(isinstance(iter({'1':23}), Iterator))   # dictprint(isinstance(iter((1,2,3)), Iterator))    # tupleprint(isinstance(iter({1,2,3}), Iterator))    # setprint(isinstance(iter('123'), Iterator))      # str
True
True
True
True
True

被 __iter__() 方法包装之后,列表、字典、元组、集合和字符串都是迭代器了。注意 iter(x) 和 x.__iter__() 是等价的,后者太难看因此习惯用前者。

既然能被迭代,那该对象里面肯定有 __next__() 魔法方法,要不然怎么可以一个元素一个元素往下走下去啊?来验证一波。

定义列表 nums,可以被 for 循环遍历。

nums = [1, 2, 3]
for num in nums:    print(num)
1
2
3

但是 nums 不是迭代器 (只是 for 循环在遍历前将其转换成迭代器了),用 next() 会不错,错误信息是“列表对象不是迭代器”。

print(next(nums))

用 iter() 将可迭代对象 nums 转换成迭代器 i_nums,打印其本身发现 i_nums 是 list_iterator,打印其下属性,找到了 __next__。

i_nums = iter(nums)print(i_nums)print(dir(i_nums))

除了 __next__,迭代器里还有 __iter__,而 __iter__ 是判断可迭代对象的标准。因此可得

只要是迭代器就是可迭代对象,但反过来不成立。

现在两者关系梳理清楚了吧,我们来用 next() 一个个打印迭代器 i_nums 里的值。

i_nums = iter(nums)
print( next(i_nums) )print( next(i_nums) )print( next(i_nums) )
1
2
3

如果打印 4 遍呢?

i_nums = iter(nums)
print( next(i_nums) )print( next(i_nums) )print( next(i_nums) )print( next(i_nums) )
1
2
3
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-98-cb9222563d74> in <module>
      4 print( next(i_nums) )
      5 print( next(i_nums) )
----> 6 print( next(i_nums) )

StopIteration:

第 6 行,在获取第 4 个元素时报错,StopIteration

有了 StopIteration 这个提示,我们甚至可以自己写代码来实现用 for 循环来打印。

i_nums = iter(nums)
while True:    try:        print(next(i_nums))    except StopIteration:        break
1
2
3
  • 第 1 行:将列表转成迭代器。
  • 第 4-5 行:如果程序没报错,打印下一个元素
  • 第 6-7 行:如果程序报 StopIteration 错,说明已经遍历结束,用 break 语句跳出。

2.3

自定义迭代器

上节讲了用 iter() 函数可以将可迭代对象转换成迭代器,本节再介绍两种定义迭代器的方法:1. 用类;2. 生成器。

用类来定义

我们已经知道迭代器里面有 __iter__ 和 __next__ 方法,那我们只需在类里自定义这两个方法了。以 MyRange 为例。

class MyRange:        def __init__(self, start, end):        self.value = start        self.end = end        def __iter__(self):        return self        def __next__(self):        if self.value >= self.end:            raise StopIteration        current = self.value        self.value += 1        return current
  • 第 3-6 行:每个类里都要有 __init__ 来构建对象,参数 start 和 end 分别代表是首尾位置,将 start 赋值给 self.value,将 end 赋值给 self.end。
  • 第 7-8 行:因为我们会用 __next__ 来使得 MyRange 是个迭代器,那么 __iter__ 返回它本身就好了。
  • 第 10-15 行:第 11-12 行在停止条件时提出 StopIteration 。第 13-14 行是将现有状态的值 self.value 赋给变量 current,将 self.value 前进 1 步。第 15 行返回 current。

测试一下用 MyRange 类定义的迭代器 nums。

nums = MyRange(1, 5)
for num in nums:    print(num)
1
2
3
4

可以被 for 循环。

nums = MyRange(1, 5)
print(next(nums))print(next(nums))print(next(nums))print(next(nums))
1
2
3
4

也可以使用 next() 函数。

用生成器来定义

用类定义迭代器没毛病,就是代码太冗长了。而用生成器定义迭代器真的简洁,代码如下。

def range_generator(start, end):    current = start    while current < end:        yield current        current += 1

代码太简单了有没有。首先将 start 复制给 current,只要 current 比 end 小,就 yield 一个 current,再加个 1 更新它。

测试一下用 range_generator 生成器定义的迭代器 nums。

nums = range_generator(1, 5)
for num in nums:    print(num)
1
2
3
4

可以被 for 循环。

nums = range_generator(1, 5)
print(next(nums))print(next(nums))print(next(nums))print(next(nums))
1
2
3
4

也可以使用 next() 函数。


思考下面生成器的输出是什么?

def range_generator(start):    current = start    while True        yield current        current += 1

从 start 开始,每次加 1 没有终点 (不建议在电脑上实验!)

2.4内置迭代器

在 Python 里有不少内置的迭代器,用起来非常方便,我们会介绍 count, cycle, repeat, combinations, permudations, product 和 chain。

首先引用 itertools,顾名思义就知道它里面有很多迭代器的工具。

import itertools
print(dir(itertools))

count

counter = itertools.count()
print(next(counter))print(next(counter))print(next(counter))print(next(counter))
0
1
2
3

count() 就像是计数器,不停的往前更新。它可用在给不知道大小的数据标注索引。比如我们在收集交易数据,未来有多少个我们不知道,我们就可以用 count() 来不停更新索引值。

下列以 4 个数据点 (假设是阿里巴巴每分钟的股票价格) 为例,用 zip 函数来将 count() 的输出给股价标注索引。

data = [170.1, 170.8, 171.4, 170.5]
minute_data = zip( itertools.count(), data)print(minute_data)
<zip object at 0x00000179D362FC88>

这时得到是个 zip 对象,可将其转换成 list 打印其内容。

print(list(minute_data))
[(0, 170.1), (1, 170.8), (2, 171.4), (3, 170.5)]

data 里面有 4 个点,count() 就返回 4 个值,如果data 里面有 4000000 个点,count() 就返回 4000000 个值。体会到使用 count() 的便利了吗?

当然,我们可以设置起始值间隔值,请参考下面两段代码。

counter = itertools.count(start=5)
print(next(counter))print(next(counter))print(next(counter))print(next(counter))
5
6
7
8
counter = itertools.count(start=5, step=-2)
print(next(counter))print(next(counter))print(next(counter))print(next(counter))
5
3
1
-1

cycle

cycle = itertools.cycle(('on','off'))
print(next(cycle))print(next(cycle))print(next(cycle))print(next(cycle))
on
off
on
off

cycle() 作用是循环遍历!

repeat

repeat = itertools.repeat(2, times=3)
print(next(repeat))print(next(repeat))print(next(repeat))print(next(repeat))
2
2
2
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-49-4e464e5eee1b> in <module>
      4 print(next(repeat))
      5 print(next(repeat))
----> 6 print(next(repeat))

StopIteration:

repeat() 作用是重复,但是一旦设置 times 参数比如 3,那么不能打印次数不能超过 3,否则会报错。

repeat() 还可以和其他高阶函数一起用,如下列 map 函数。将 pow 操作 (第一个参数) 作用在 [0,1,2,3,4] 上,指数为 2。用 repeat() 好处是重复的次数会跟前面 range(n) 匹配。

square = map( pow, range(5), itertools.repeat(2) )square
<map at 0x179d3724908>
list(square)
[0, 1, 4, 9, 16]

combinations & permutations

接下来看看用于排列 (permutation) 和组合 (combination) 的迭代器。

首先创建一个列表 letters,一个元组 numbers 和一个集合 names。

letters = ['a', 'b', 'c', 'd']numbers = (1, 2, 3, 4)names = {'Steven', 'Sherry'}

从 letters 里面 4 个元素取出 2 个来组合 (元素位置不重要)。

results = itertools.combinations(letters, 2)
for items in results:    print(items)
('a', 'b')
('a', 'c')
('a', 'd')
('b', 'c')
('b', 'd')
('c', 'd')

从 letters 里面 4 个元素取出 2 个来排列 (元素位置重要)。

results = itertools.permutations(letters, 2)
for items in results:    print(items)
('a', 'b')
('a', 'c')
('a', 'd')
('b', 'a')
('b', 'c')
('b', 'd')
('c', 'a')
('c', 'b')
('c', 'd')
('d', 'a')
('d', 'b')
('d', 'c')

product

product() 是穷举出所有情况,本例只从 numbers 里面 4 个元素取出 2 个,不同位置元素可以重复。

results = itertools.product(numbers, repeat=2)
for items in results:    print(items)
(1, 1)
(1, 2)
(1, 3)
(1, 4)
(2, 1)
(2, 2)
(2, 3)
(2, 4)
(3, 1)
(3, 2)
(3, 3)
(3, 4)
(4, 1)
(4, 2)
(4, 3)
(4, 4)

上面结果去除一些「位置不同但元素相同」,比如 (1, 2) 和 (2, 1) 只保留一个,就是 combinations_with_replacement() 的结果,验证如下。

results = itertools.combinations_with_replacement(numbers, 2)
for items in results:    print(items)
(1, 1)
(1, 2)
(1, 3)
(1, 4)
(2, 2)
(2, 3)
(2, 4)
(3, 3)
(3, 4)
(4, 4)

上面结果再去除「重复元素」,比如 (1, 1), (2, 2), ... ,就是 combinations() 的结果验证如下。

results = itertools.combinations(numbers, 2)
for items in results:    print(items)
(1, 2)
(1, 3)
(1, 4)
(2, 3)
(2, 4)
(3, 4)

chain

用于三个不同类型的数据格式,列表 letters,元组 numbers 和集合 names,我们可用 chain() 将它们串起来成一个迭代器,再逐个遍历它里面的元素。

代码如下。

results = itertools.chain(letters, numbers, names)
for items in results:    print(items)
a
b
c
d
1
2
3
4
Sherry
Steven

3.总结

在 Python 里,

  • 字典用来创建映射关系
  • 函数用来创建可调用对象
  • 生成器用来创建迭代器

当你想要个可用惰性计算的可迭代对象时,考虑用迭代器

当你想创建迭代器时,考虑用生成器

当你想创建生成器时,考虑用生成函数 (用 yield) 或生成表达式 (用小括号 ())。

当你想 ...,别想了,在看-转发-留言吧,千万不要赞赏我。下篇讲装饰器 (Decorator)。

Stay Tuned!

本文分享自微信公众号 - 王的机器(MeanMachine1031),作者:王圣元

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 盘一盘如何「体系化」学习 Python 基础知识

    将零碎的知识点体系化真的很重要,我把 Python 基础的所有要点都放在一张思维脑图(Xmind 做的)里了。不得不说思维导图真是体系化知识的好工具。

    用户5753894
  • 解读吴恩达新书的全球第一帖 (中)

    吴恩达 (之后称大神) 在 2018 年 5 月 23 日北京时间早上 6 点 15 分将《Machine Learning Yearning》一书更新到第 3...

    用户5753894
  • 盘一盘 Python 系列特别篇 - 面向对象编程

    在写 Keras (下) 时,发现很多内容都要用到类 (class) 和对象 (object),因此本文作为 Python 系列的特别篇,主要介绍面向对象编程 ...

    用户5753894
  • Python3 迭代器与生成器

    迭代是Python最强大的功能之一,是访问集合元素的一种方式。 迭代器是一个可以记住遍历的位置的对象。 迭代器对象从集合的第一个元素开始访问,直到所有的元素...

    用户2398817
  • 剑指offer第四天

    25.复杂链表的复制 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的hea...

    郭耀华
  • 项目管理——实践入门

    项目管理——实践入门 前言: 项目管理的作用对象是项目团队(当然也有项目外部的干系人,本文只针对项目团队),最好的项目管理应该是让团队有清晰统一的目标、亲密无间...

    奋斗蒙
  • Python学习 :迭代器&生成器

    定义生成器可以使用yield关键词。在Python中,它作为一个关键词,是生成器的标志

    用户2398817
  • python一些常用小技巧

    这个方法可以将布尔型的值去掉,例如(False,None,0,“”),它使用 filter() 函数。

    赵云龙龙
  • Python | 数据挖掘,WordCloud词云配置过程及词频分析

    其中WordCloud是词云,jieba是结巴分词工具。 问题:在安装WordCloud过程中,你可能遇到的第一个错误如下。

    用户1634449
  • 5.迭代器和生成器

    zhang_derek

扫码关注云+社区

领取腾讯云代金券