Python-迭代器、生成器、推导式
可迭代对象
迭代(Iteration),是指通过遍历获取某容器内所有元素,特指遍历获取这个动作。for对数据进行遍历,这个操作就是迭代。
可迭代(Iterable),是指某个容器类型数据可被for循环遍历获取内部所有成员。这个数据类型就称为可迭代。
对象(Object)
Python 从设计之初就是一门面向对象的语言,他有一个重要的概念,一切皆对象。数字、字符串、元组、列表、字典、函数、方法、类、模块文件等都是对象,包括我们编写的代码。
对象,也叫实例(Instance),可以理解为是一个记录和管理数据的容器,它的成员是属性(property,变量,属性的值就是数据)和方法(method,就是操作该对象以及对象内部数据的函数)。
字符串就是一个对象,因此,我们可以通过字符串,调用字符串提供的操作来完成字符串的转换大小写,替换,分割等操作。当然,大部分对象都具备了0个属性或多个属性,也可能具备0个内部方法或者多个方法。
示例:
点击查看代码
class Student(object):
def __init__(self, name, age):
self.name = name # self 自身,自己,我的
self.age = age
def introduction(self):
print(f"我叫{self.name}, 今年{self.age}岁")
stu1 = Student(name="小明", age=16)
print(stu1) # <__main__.Student object at 0x000001BB13A20FD0>
print(stu1.age) # 16
stu1.introduction() # 我叫小明,今年16岁
上面代码表达的意思:对象是一个记录和管理数据的容器,它的成员是属性(例如:name 和 age)和方法(例如:init, introduction)。
- 从使用角度来讲,能被for循环遍历输出内部每一个成员的都是可迭代对象。
- 从语法形式来讲,能调用__iter__方法的数据对象就是可迭代对象。
我们来编写一个代码看看python提供的可迭代对象。常见的有: str, list, tuple, dict, set, 文件对象等。
点击查看代码
ret = "hello world".__iter__()
print(ret) # <str_iterator object at 0x000001D5B9D5F2E0>
ret = [1, 2, 3].__iter__()
print(ret) # <list_iterator object at 0x00000279FB150FA0>
ret = {'name':'xiaoming'}.__iter__()
print(ret) # <dict_keyiterator object at 0x00000279FAE70630>
ret = {7, 8, 9}.__iter__()
print(ret) # <set_iterator object at 0x00000279FB132A40>
简单判断一个数据是否是可迭代对象
set_data = {'a',2,'c','b'}
for i in set_data:
print(i)
# dir() 获取对象所有内置方法
data = dir(set_data)
print(data)
print('__iter__' in data) # True
迭代器
- 迭代器对象,简称迭代器,是增强版的可迭代对象。
- 任何一个对象,只要内部实现了__iter__()就是一个可迭代对象(Iterable)。
专业的说,就是实现了可迭代协议的对象,叫可迭代对象。
- 任何一个对象,只要内部实现了__iter__()和__next__()就是一个迭代器(Iterator)。
专业的说,就是实现了迭代器协议的对象,叫迭代器。
所有,迭代器一定是一个可迭代对象,可迭代对象不一定是迭代器。
把一个可迭代对象变成迭代器对象有 2 种方法:
方式1:所有的可迭代对象都有一个__iter__(),该方法的作用就是把可迭代对象转换成迭代器对象。
可迭代对象.__iter__()
方式2: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后面数据对象x必须是可迭代对象。
for循环首先会调用可迭代对象data内的__iter__方法返回一个迭代器,然后再调用这个迭代器的__next__方法将取得的值赋给item,即for后面定义的变量item(item经过每次for循环调用next()时都会赋值了一遍)。循环一次,调用一次next方法,直至遇到StopTteration异常,for循环内部捕获并处理该异常后结束迭代过程。
while循环模拟遍历过程:
"""
while比for循环要单调一点,仅仅是重复,但是我们可以通过while模拟for循环这个过程。
"""
data = [10,20,30,40]
# 1、先执行iter方法,把可迭代对象转换成迭代器
iter_data = iter(data)
while True:
item = next(iter_data) # 也可以使用iter_data.__next__(),但是官方编码规范,建议使用next(iter_data)
print(item) # 循环中如果出现异常就会导致循环终止,python利用StopIteration异常,达到终止循环的目的,为了防止无线循环下去
while循环模拟捕获处理异常过程
"""
while 循环模拟捕获处理异常过程
# python提供的异常处理语句,可以让识别代码异常而不会导致代码因为异常出错而终止
try:
python 代码
except 异常类型:
当报错了,则python会自动终止try的代码继续执行,改成execpt语句的代码
"""
data = [10, 20, 30, 40]
# 1、先执行iter方法,把迭代对象转换成迭代器
iter_data = iter(data)
while True:
try:
# 2、每次循环,通过next取值,付给item
item = next(iter_data)
print(item)
except:
break # 此处,退出循环
# 使用while遍历一个这样的数据非常麻烦,所以都是推荐使用for循环。
自定义迭代器
点击查看代码
# 斐波那契数列
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.a
self.a, self.b = self.b, self.a + self.b
self.n = self.n + 1
return r
raise StopIteration()
for i in Fib(8):
print(i)
可迭代对象与迭代器的比较
- 可迭代对象的优点:
1、访问速度快。因此一切数据都在内存中。
2、访问方式灵活,可多次、重复、任意选择范围访问。
3、内置方法和函数比较多。- 可迭代对象的缺点:
1、耗费内存,数据一次性存储在内存空间
2、取值过于灵活,不限制方向,有时候不当的操作会出现异常(例如:index out of range)- 迭代器的优点:
1、惰性序列,惰性序列,挤牙膏,每次取一条数据放同一个内存中,直到取值完毕。
2、节省内存空间,可用于遍历读取大文件,海量数据。- 迭代器的缺点:
1、访问速度慢。取值,赋值,不断重复,所以会比寻常的可迭代对象直接从内存中取值肯定要慢。
2、内存操作比较少。经过iter包装,所以很多的操作不能直接调用。
3、访问方式死板,方向不可逆,不能反复,只能向下取值。
为什么有了迭代器以后,还要可迭代对象呢?
迭代器的本质是节约内存,可以惰性执行,因此肯定是好的。但是,编程的本质是为了完成我们工作,那么工作慢了肯定不好所以,我们不能为了节约内存而让我们的编程变慢。
肯定是希望能运行的程序越快越好,因此可迭代对象在内存足够的情况下,肯定比迭代器要好。
所以,这就是为什么默认情况下python提供的所有基础容器类型数据都是可迭代对象。
生成器
生成器对象,简称生成器(generator) 算得上是Python语言中最吸引人的特性之一。
生成器是一种特殊的迭代器,不过这种迭代器更加优雅。它不需要再像上面一样定义__iter__()和__next__()方法了,只需要在函数中使用到一个yield关键字,所以也叫生成器函数。
生成器是一种特殊的迭代器,因此生成器也具备了迭代器的特点和缺点,例如惰性序列,惰性执行,节省内存空间,不可逆向取值等。
声明和使用
简单来说,一个函数里面只要出现了yield关键字,那么就是一个生成器函数,所以生成器函数的用法与函数类似,比函数功能更多。
def gen():
yield 10
yield 20
yield 30
# 调用生成器函数,返回值是一个生成器对象
g1 = gen()
# 生成器对象只能用于迭代遍历, 或者next()取值。
for item in g1:
print(item)
"""
10
20
30
"""
# 每次调用生成器函数,都会产生一个新的生成器对象。
g2 = gen()
for item in g2:
print(item)
"""
10
20
30
"""
print(dir(g2)) # 虽然没有显示的声明__iter__和__next__,但是这个生成器既然是一个特殊的迭代器,那么肯定有__iter__和__next__
# ['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
生成器是一个特殊迭代器,也是一个特殊函数,内部可以有1个或多个yield关键。
因为生成器是一个特殊函数,所有也具备函数的特点,有参数,有返回值等等。
"""生成器的声明"""
# 生成器函数
def gen():
yield 1
yield 5
yield "hello"
"""生成器的基本使用"""
g1 = gen() # 生成器函数的执行结果,是一个生成器对象(generator object).
print(g1) # <generator object gen at 0x0000017DCF2A8DD0>
对生成器的使用,主要可以依靠for循环和next()操作,开发中基本不适用next,所以重点是for循环。
点击查看代码
for item in g1:
print(item)
"""
循环结果:
1
5
hello
"""
# 每次调用生成器函数,都会产生一个新的生成器对象。
g2 = gen()
print(g2) # <generator object gen at 0x000002DB15E98D60>
for item in g2:
print(item)
"""
循环结果:
1
5
hello
"""
# 每次调用生成器函数,都会产生一个新的生成器对象。
g3 = gen()
print(g3) # <generator object gen at 0x0000014E255D8DD0>
ret = g3.__next__()
print(ret)
ret = g3.__next__()
print(ret)
ret = g3.__next__()
print(ret)
# ret = g3.__next__()
# print(ret) # StopIteration
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) # ('A', 'B')
ret = g1.__next__()
print(ret) # ('C', 'D')
ret = g1.__next__()
print(ret) # ('E', 'F')
ret = g1.__next__()
print(ret) # StopIteration
"""
上面代码可以观察到,生成器函数调用时只会返回一个生成器对象(并没有执行函数中的代码)。
只有当生成器对象调用__next__方法时才会触发函数体代码执行,直到遇到关键字yield暂停,将yield后的值作为返回值返回next()调用处,
所以yield类似于return的功能,但不同于return的是,return返回,函数结束;
而yeild将函数的状态记录并暂停,等待生成器对象再次调用__next__方法时,函数从原来暂停的位置后的第一条语句继续运行直到再遇见yield并返回其后的值,又再次暂停;
如果不断调用__next__方法,最后一次进入函数体,待执行代码不再有yield此时报出迭代异常(StopIteration),表示迭代结束。
"""
用生成器来实现斐波那契数列:
点击查看代码
# 用生成器来实现斐波那契数列
def fib(max):
n, a, b = 0, 0, 1 # n 表示当前项,a 表示第一项的值,b 表示第二项的值
while n < max:
yield a
a, b = b, a+b
n += 1
g1 = fib(8)
print(g1) # <generator object fib at 0x00000181E4798E40>
for i in g1:
print(i)
"""
0
1
1
2
3
5
8
13
"""
# 因为这个生成器会记录和保存程序执行的状态,因此,当生成器结束后,就不会再逆向取值,所以下面的代码拿不到数据的,因为上面已经取完了。
for i in g1: # 此时函数不会执行,因生成器的数据已经取完。
print("test")
print(i)
next()尽可以从生成器中获取yield的返回值,而send()不仅可以从生成器中获取yield的返回值,还可以发送数据(传参)到生成器内部给上一个yield(也就是暂停出)接收到。
点击查看代码
def gen2():
key = 0
print(">>>>> 开始 <<<<<")
while True:
food = yield "第%s次" %key
print(f"内部接收到了{food}")
key += 1
g2 = gen2()
ret = g2.send(None) # >>>>> 开始 <<<<<
# 在循环之前,先执行一般send的作用是为了预激活生成器。
# 让生成器内部执行到第一个yield位置,否则代码没有暂停的话,就无法通过send传递数据给内部的yield
for item in ["电视", "冰箱"]:
res = g2.send(item)
print(f"res={res}")
"""
内部接收到了电视
res=第1次
内部接收到了冰箱
res=第2次
"""
因为生成器中yield关键字的特征是暂停函数执行,所以可以让多个任务程序(函数)交替执行。这也是网络编程和并发编程里面的协程的实现原理。
点击查看代码
import time
def gen1():
while True:
print("gen1--1")
yield
print("gen1--2")
print("休息5秒")
time.sleep(5)
def gen2():
while True:
print("gen2--3")
yield
print("gen2--4")
print("休息5秒")
time.sleep(5)
# 判断当前文件是否作为主程序入口来运行
# 如果__name__魔术变量的值为“__main__”则表示当前文件是主执行文件
# 如果__name__魔术变量的值为文件名,则表示当前文件不是主执行文件
if __name__ == "__main__":
g1 = gen1()
g2 = gen2()
for i in range(3):
next(g1)
print("for循环!")
next(g2)
"""
gen1--1
for循环!
gen2--3
gen1--2
休息5秒
gen1--1
for循环!
gen2--4
休息5秒
gen2--3
gen1--2
休息5秒
gen1--1
for循环!
gen2--4
休息5秒
gen2--3
"""
yield from
yield from 可以将一个迭代对象变成一个迭代器返回,也可以用于多个生成器之间进行嵌套调用。因此yield from 也叫委派生成器(委托生成器)。
点击查看代码
def gen():
data = ["1", "2", "3", "4"]
# yield data # ['1', '2', '3', '4']
yield from data
g1 = gen()
# for item in g1:
# print(item)
"""
1
2
3
4
"""
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))
"""
++++++++
>>>> 0
++++++++
0
>>>> 1
++++++++
1
>>>> 2
++++++++
4
>>>> 3
++++++++
9
>>>> 4
++++++++
16
"""
# 希望还是实践操作一下吧,然后debug一下看一下程序流程,能更清晰的了解逻辑。
推导式
推导式,实际上就是一种代码简写方式。是通过一行代码完成一个或多个循环和判断,并遍历出一系列数据的编写代码方式。
# 语法:
# 成员 for 循环 ... if 判断 ...
# 推导式种类有4种:
1. 列表推导式,结果是一个列表
[item for item in Iterable]
2. 字典推导式,结果是一个字典
{a:b for a,b in iterable.items()}
3. 集合推导式,结果是一个集合
{item for item in Iterable}
4. 生成器表达式,结果是一个生成器
(item for item in Iterable)
# 其中,列表和字典最常用
列表推导式
点击查看代码
# 列表推导式,推导式简写规则:按嵌套的顺序,逐个简写,从外到内,从左往右
ret = []
for 1....
if 1...
for 2...
if 2...
ret.append(item)
[item for 1... if 1... for 2... if 2...]
"""1. 把列表中的每一个数字进行取整。"""
data = [3, 30.5, 9.99, 10.03]
# 方案一:
data01 = []
for i in data:
data01.append(int(i))
print(data01) # [3, 30, 9, 10]
# 推导式:
ret = [int(item) for item in data]
print(ret) # [3, 30, 9, 10]
"""2. 找出1~100之间的偶数"""
# 方案一:
data = []
for i in range(1, 101):
if i % 2 ==0:
data.append(i)
print(data)
# [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100]
# 推导式:
ret = [i for i in range(1, 101) if i % 2 == 0]
print(ret)
# [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100]
"""2.1. 找出1~100之间不能被7整除的数"""
# 方案一:
list01 = []
for i in range(1, 101):
if not i % 7 == 0:
# print(i)
list01.append(i)
print(list01)
# 推导式:
ret = [i for i in range(1, 101) if not i % 7 == 0]
print(ret)
"""3. 找出列表中年龄大于12的男生"""
data = [
{"name": "小明", "age": 12},
{"name": "大明", "age": 11},
{"name": "小白", "age": 13},
{"name": "大白", "age": 15},
]
# 方案一:
data01 = []
for item in data:
if item["age"] > 12:
data01.append(item)
print(data01) # ['小白', '大白']
# 推导式:
ret = [item["name"] for item in data if item["age"] > 12]
print(ret) # ['小白', '大白']
"""4. 给两个列表成员的男生与女生配对,列出所有组合。"""
female = ["小白","小红","小花"]
male = ["小黑","小绿","小草"]
list = [(nv, nan) for nv in female for nan in male]
print(list)
# [('小白', '小黑'), ('小白', '小绿'), ('小白', '小草'), ('小红', '小黑'), ('小红', '小绿'), ('小红', '小草'), ('小花', '小黑'), ('小花', '小绿'), ('小花', '小草')]
集合推导式
点击查看代码
# 1. 计算列表中每个值的平方,自带去重功能
data01 = [1, -1, 2, -3, 3]
ret = {item**2 for item in data01}
print(ret) # {1, 4, 9}
# 2. 对列表中的所有数字进行取整,并去重保留偶数
data02 = [11.5, 10.5, 10.3, 11.8, 12.0]
ret = {int(item) for item in data02 if int(item) % 2 == 0}
print(ret) # {10, 12}
# 3. 找出列表中所有人的姓氏,重复去除。
data03 = ["张小明", "王小龙", "李大白", "李小龙", "张三风", "张无忌"]
ret = {item[0] for item in data03}
print(ret) # {'张', '王', '李'}
字典推导式
点击查看代码
字典推导式
{键:值 for ... if ... }
# 1. 对字典中每个成员的值进行取整
data01 = {'B': 10.50, 'A': 50.33}
ret = {int(v) for v in data01.values()}
print(ret) # {10, 50}
# 2. 统计列表中各个字符出现的次数
data02 = ["A", "B", "C", "A", "C", "A"]
ret = {item:data02.count(item) for item in data02}
print(ret) # {'A': 3, 'B': 1, 'C': 2}
# 3. 把字典中大小写对应的成员进行数值相加,并把键改成大写
data03 = {'a': 10, 'b': 34, 'A': 7, 'C': 3, "c": 10}
ret = {k.upper():data03.get(k.lower(), 0) + data03.get(k.upper(), 0) for k in data03.keys()}
print(ret) # {'A': 17, 'B': 34, 'C': 13}
生成器表达式
点击查看代码
# 获取1~10的整数的平方
N = (i**2 for i in range(1,11))
print(N) # <generator object <genexpr> at 0x000002AFE17D9E40>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?