迭代器
可迭代对象
讲迭代器之前,我们先了解一个概念:可迭代对象(Iterable
)。
那么什么是可迭代?什么是对象?
迭代(Iteration),是指通过遍历获取某容器内所有元素,特指遍历获取这个动作。
可迭代 (iterable),是指某个容器类型数据可被for循环遍历获取内部所有成员。
对象(Object),我们会在后面的面向对象部分专门介绍。我们现在先简单了解下什么是对象。
Python从设计之初就是一门面向对象的语言,它有一个重要的概念,一切皆对象。数字、字符串、元组、列表、字典、函数、方法、类、模块文件等都是对象,包括我们编写的代码。
对象,也叫实例,可以理解为是一个记录和管理数据的容器,它的成员是属性(property, 变量,属性的值就是数据)和方法(method, 就是操作该对象以及对象内部数据的函数)。
像我们之前学习的字符串,就是一个对象,因此,我们之前可以通过字符串,调用字符串提供的操作来完成字符串的转换大小写,替换,分割等操作。当然,大部分对象都具备了
# 以下是对象的操作示例代码:
class Student(object):
def __init__(self, name, age):
self.name = name
self.age = age
def introduction(self):
print(f"我叫{self.name},今年{self.age}岁")
stu1 = Student(name="小明", age=16)
print(stu1) # <__main__.Student object at 0x0000020F5A084DC0>
print(stu1.age) # 16
stu1.introduction() # 我叫小明,今年16岁
stu2 = Student(name="小白", age=13)
print(stu2) # <__main__.Student object at 0x000001BC0C7B11F0>
print(stu2.age) # 13
stu2.introduction() # 我叫小白,今年13岁
从使用角度来说,能被for循环遍历输出内部每一个成员的都是可迭代对象。
从语法形式来说,能调用__iter__
方法的数据对象就是可迭代对象。
我们先编写一个代码来看看python提供的可迭代对象。常见的有:str,list ,tuple,dict,set,文件管道。
ret = "hello".__iter__()
print(ret) # <str_iterator object at 0x000002365279A520>
ret = [1,2,3].__iter__()
print(ret) # <list_iterator object at 0x10221b150>
ret = {'name':'xiaoming'}.__iter__()
print(ret) # <dict_keyiterator object at 0x1022180a8>
ret = {7,8,9}.__iter__()
print(ret) # <set_iterator object at 0x1021ff9b0>
简单判断一个数据是否是可迭代对象。
set_data = {'a',2,'c','b'}
for i in setvar:
print(i)
# dir() 获取对象所有内置方法
data = dir(set_data)
print(data)
print('__iter__' in data) # True
迭代器
迭代器对象,简称迭代器,是增强版的可迭代对象。
任何一个对象,只要内部实现了__iter__()
就是一个可迭代对象(iterable)。当然专业的说,就是实现了可迭代协议的对象。
任何一个对象,只要内部实现了__iter__()
和__next__()
就是一个迭代器(iterator)。当然专业的说,就是实现了迭代器协议的对象。
所以,迭代器一定是可迭代对象,可迭代对象不一定是迭代器。
把一个可迭代对象变成迭代器对象有2种写法:
# 方式1:所有的可迭代对象都有一个__iter__(),该方法的作用就是把可迭代对象转换成迭代器对象。
可迭代对象.__iter__()
# 方式1:python内置函数iter转换,两个本质上一样,iter(可迭代对象) 就是 可迭代对象.__iter__()
iter(可迭代对象)
查看一个对象是否是可迭代对象或迭代器:
from collections.abc import Iterable, Iterator
data = [1, 2, 3, 4]
print(isinstance(data, Iterable)) # True # 查看是不是可迭代对象
print(isinstance(data, Iterator)) # False # 查看是不是迭代器
# 在 python中容器数据类型都是可迭代对象,但是并非迭代器,那么我们就可以进行装换。
# 方式1:
print(isinstance(data.__iter__(), Iterator)) # True
# 方式2:
print(isinstance(iter(data), Iterator)) # True
迭代器的特性就是能够调用__next__
方法依次计算出迭代器中的每一个值。基于此就可以实现无论是否数据为序列对象,都可以通过迭代取值的方式完成读取成员值的操作。
set_data = {"A", "B", "D", 100}
# data = iter(set_data) # 方法一
data = set_data.__iter__() # 方法二
print(data)
res = data.__next__()
print(res)
res = data.__next__()
print(res)
res = next(data) # next(data),就是 data.__next(),本质上没有区别,仅仅写法不同。
print(res)
res = next(data)
print(res)
# 迭代器中所有值都被提取完成后,再次取值,python会以抛出一个StopIteration异常告诉我们,没有值了。
# 这并不代表错误发生,而是一种迭代完成的标志,防止出现无限循环。
# res = next(data)
上面的代码操作可以发现,迭代器迭代取值的过程,并非通过索引而是通过__next__
方法或next()函数来完成的。
for循环的本质
之前的学习只知道for循环是用来遍历某个数据对象的。但for循环内部到底是怎么工作的,关键字in后面可以放什么数据类型呢?
x = [1, 2, 3]
# for循环的形式:
for item in x:
print(item)
关键字in后面数据对象data必须是可迭代对象。
for 循环首先会调用可迭代对象data内的__iter__
方法返回一个迭代器,然后再调用这个迭代器的next方法将取到的值赋给item,即for后面定义的变量item。循环一次,调用一次next方法,直至遇到StopIteration异常,for循环内部捕获并处理该异常后结束迭代过程。
while循环模拟遍历过程
data = iter([10,20,30,40])
while True:
item = data.__next__()
print(item)
while循环模拟捕获处理异常过程
data = iter([10,20,30,40])
while True:
try:
item = data.__next__()
print(item)
except StopIteration:
break
自定义迭代器
因为我们没有学到面向对象,所以以下代码,大家可以了解一下即可,该例子的用途仅仅只是为了演示迭代器的用途而已。
class Fib(object):
def __init__(self, max):
self.max = max
self.n, self.a, self.b = 0, 0, 1
def __iter__(self):
return self
def __next__(self):
if self.n < self.max:
r = self.b
self.a, self.b = self.b, self.a + self.b # 这次结果作为下次的初始值
self.n = self.n + 1
return r
raise StopIteration()
for i in Fib(10):
print(i)
可迭代对象与迭代器比较
可迭代对象的优点:
- 访问速度快。
- 访问方式灵活,可多次、重复、任意选择范围访问。
- 内置方法和函数比较多。
可迭代对象的缺点:
- 耗费内存,数据一次性存储在内存空间。
- 取值过于灵活,不限制方向,有时候会操作(例如:index out of range)
迭代器的优点:
- 惰性序列,惰性执行,挤牙膏,一次取一条数据,直到取值完毕
- 节省内存空间,可用于遍历读取大文件,海量数据。
迭代器的缺点:
- 访问速度慢。
- 内置操作比较少。
- 访问方式死板,方向不可逆,不能反复,只能向下取值。
生成器
生成器对象,简称生成器(generator)算得上是Python语言中最吸引人的特性之一。
生成器是一种特殊的迭代器,不过这种迭代器更加优雅。它不需要再像上面一样定义__iter__()
和__next__()
方法了,只需要在函数中使用到一个 yiled
关键字,所以也叫生成器函数。
生成器是一种特殊的迭代器,因此生成器也具备了迭代器的特点,例如惰性序列,惰性执行,节省内存空间等。
声明和使用
# 简单来说,一个函数里面只要出现了yield关键字,那么就是一个生成器函数。所以生成器函数的用法与函数类似,比函数功能更多。
def gen():
yield 100
yield 200
yield 300
# 调用生成器函数,返回值是一个生成器对象
g1 = gen()
# 生成器对象只能用于迭代遍历,或者next()取值。
for item in g1:
print(item)
# 每次调用生成器函数,都会产生一个新的生成器对象。
g2 = gen()
for item in g2:
print(item)
yield
yield 是一个python内置的关键字,它的作用有一部分类似return,可以把函数内部的数据提供给外界调用处。但是不同的是,return 会终止函数的执行,yield 不会终止函数的执行,而是暂停了。两者都会返回一个结果,但return只能执行一次给函数的调用处返回值,而yield是可以执行多次给next()方法返回值,而且yield还可以接受外界send()方法的传值。所以更准确的来说,yield是暂停程序的执行权并记录了程序当前的运行状态的关键字,同时也是生成器内部与外界进行数据传递的通道。
def gen(): # 生成器返回值
yield "A", "B"
yield "C", "D"
yield "E", "F"
g1 = gen()
ret = g1.__next__()
print(ret)
ret = g1.__next__()
print(ret)
ret = g1.__next__()
print(ret)
# ret = g1.__next__()
# print(ret)
上面代码可以观察到,生成器函数调用时只会返回一个生成器对象。只有当生成器对象调用__next__
方法时才会触发函数体代码执行,直到遇到关键字yield停止,将yield后的值作为返回值返回,所以,yield类似于return的功能,但不同于return的是,return返回,函数结束;而yield将函数的状态挂起,等待生成器对象再次调用__next__
方法时,函数从挂起的位置后的第一条语句继续运行直到再遇见yield并返回其后的值;如果不断调用__next__
方法,最后一次进入函数体,待执行代码不再有yield此时报出迭代异常的错误。
用生成器来实现斐波那契数列
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
for i in fib(10):
print(i)
next()仅可以从生成器中获取yield的返回值,而send()不仅可以从生成器中获取yield的返回值,还可以发送数据到生成器内部给上一个yield接收到。
def gen2():
key = 0
print(">>>>> 嘟嘟,开车了")
while True:
food = yield "第%s次" % key
print(f"内部接收到了{food}")
key +=1
g3 = gen2()
g3.send(None) # g3.__next__() 预激活生成器,让生成器内部执行到第一个yield位置,否则无法通过send传递数据给内部的yield
for item in ["苹果","芒果"]:
res = g3.send(item)
print(f"res={res}")
因为生成器中yield关键字的特征是暂停函数执行,所以可以让多个任务程序交替执行。这也是协程的实现原理,后面我们学习的异步编程里面协程课程内容基础。
import time
def gen1():
while True:
print("--1")
yield
print("--2")
time.sleep(1)
def gen2():
while True:
print("--3")
yield
print("--4")
time.sleep(1)
if __name__ == "__main__":
g1 = gen1()
g2 = gen2()
for i in range(3):
next(g1)
print("主程序!")
next(g2)
yield from
yield from可以将一个可迭代对象变成一个迭代器返回,也可以用于多个生成器之间进行嵌套调用。因此yield from 也叫委派生成器。
def gen():
data = ["A","B","C","D"]
# yield data
yield from data
g1 = gen()
for item in g1:
print(item)
def gen1():
a = 0
while True:
# print("+++++++")
a = yield a**2
def gen2(gen):
yield from gen
if __name__ == '__main__':
g1 = gen1()
g2 = gen2(g1)
g2.send(None)
for i in range(5):
# print(">>>> %s" % i)
print(g2.send(i))
课堂作业
1. yield和return的区别都有哪些?
2. 可迭代对象、迭代器、生成器三者的关系?
推导式
推导式,实际上就是一种代码简写方式。是通过一行代码完成循环判断,并遍历出一系列数据的编写代码方式。语法:
成员 for 循环 ... if 判断 ...
推导式种类有4种:
# 1. 列表推导式,结果是一个列表
[item for item in Iterable]
# 2. 集合推导式,结果是一个集合
{item for item in Iterable}
# 3. 字典推导式,结果是一个字典
{a:b for a,b in iterable.items()}
# 4. 生成器表达式,结果是一个生成器
(item for item in Iterable)
列表推导式
# 1. 把列表中的每一个数字进行取整。
data = [3, 30.5, 9.99, 10.03]
ret = [int(item) for item in data]
print(ret) # [3, 30, 9, 10]
# 2. 找出1~100之间的偶数
data = [i for i in range(101) if i % 2 == 0]
print(data)
# [0, 2, 4, ...., 100]
# 3. 找出列表中年龄大于12的男生
data = [
{"name": "小明", "age": 12},
{"name": "大明", "age": 11},
{"name": "小白", "age": 13},
{"name": "大白", "age": 15},
]
ret = [item["name"] for item in data if item["age"]>12]
print(ret) # ['小白', '大白']
# 4. 给两个列表成员的男生与女生配对,列出所有组合。
female = ["小白","小红","小花"]
male = ["小黑","小绿","小草"]
lst = [(i, j) for i in female for j in male]
print(lst)
# [('小白', '小黑'), ('小白', '小绿'), ('小白', '小草'), ('小红', '小黑'), ('小红', '小绿'), ('小红', '小草'), ('小花', '小黑'), ('小花', '小绿'), ('小花', '小草')]
集合推导式
# 1. 计算列表中每个值的平方,自带去重功能
data = [1,-1,2,-3, 3]
ret = {item**2 for item in data}
print(ret) # {1, 4, 9}
# 2. 对列表中的所有数字进行取整,并去重保留偶数
data = [11.5, 10.5, 10.3, 11.8, 12.0]
ret = {int(item) for item in data if int(item) % 2 == 0}
print(ret) # {10, 12}
# 3. 找出列表中所有人的姓氏,重复去除。
data = ["张小明", "王小龙", "李大白", "李小龙", "张三风", "张无忌"]
ret = {item[0] for item in data}
print(ret) # {'李', '王', '张'}
字典推导式
# 1. 对字典中没有成员的值进行取整
data = {'B': 10.50, 'A': 50.33}
ret = {k: int(v) for k,v in data.items()}
print(ret)
# 2. 统计列表中各个字符出现的次数
data = ["A", "B", "C", "A", "C", "A"]
ret = {item: data.count(item) for item in data}
print(ret) # {'A': 3, 'B': 1, 'C': 2}
# 3. 把字典中大小写对应的成员进行数值相加,并把键改成大写
data = {'a': 10, 'b': 34, 'A': 7, 'C': 3, "c": 10}
ret = {k.upper(): data.get(k.lower(), 0) + data.get(k.upper(), 0) for k in data.keys()}
print(ret)
生成器表达式
# 获取1~10的整数的平方
N = (i**2 for i in range(1,11))
课堂作业
1. 把列表中所有字符变成小写
data = ["ADDD","dddDD","DDaa","sss"]
2. 翻转字典成员的键值
3. 找到嵌套列表中名字,首尾是同一个字的人。
4. 把列表中长度小于3的字符串成员剔除
5. 生成所有大小写的字母列表
6. 有两个列表,分别存放了去过两个不同城市工作的人员名单
beijing = ['小明','小白','小黑','小蓝','小五']
shanghai =['小红','小白','小黑','小李']
6.1. 取出既去过beijing又去过shanghai的名单列表
6.2. 取出只去过beijing,而没有去过shanghai的名单列表
6.3. 取出只去过shanghai,而没有去过beijing的名单列表