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>
posted @   将进酒_杯莫停  阅读(66)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示