学习python第十二节课:推导式与迭代器

一. 列表推导式

作用:用一个表达式创建一个有规律的列表或控制一个有规律列表。

列表推导式又叫列表生成式。

1.1 快速体验

需求:创建一个0-10的列表。

  • while循环实现
# 1. 准备一个空列表
list1 = []# 2. 书写循环,依次追加数字到空列表list1中
i = 0
while i < 10:
    list1.append(i)
    i += 1print(list1)
  • for循环实现
list1 = []
for i in range(10):
    list1.append(i)print(list1)
  • 列表推导式实现
list1 = [i for i in range(10)]
print(list1)

1.2 带if的列表推导式

需求:创建0-10的偶数列表

  • 方法一:range()步长实现
list1 = [i for i in range(0, 10, 2)]
print(list1)
  • 方法二:if实现
list1 = [i for i in range(10) if i % 2 == 0]
print(list1)
  • 方法三: for循环加if实
list1 = []
for i in range(10):
    if i % 2 == 0:
        list2.append(i)
print(list2)

通过对比可以发现,生成同样内容的列表,列表生成式的方法所使用的代码更少,更加简洁,让代码看起来更加的清爽,而这还不是列表生成式的唯一优点。

1.3 多个for循环实现列表推导式

需求:创建列表如下:

[(1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]

代码如下:

list1 = [(i, j) for i in range(1, 3) for j in range(3)]
print(list1)

二. 字典推导式

思考:如果有如下两个列表:

list1 = ['name', 'age', 'gender']
list2 = ['Tom', 20, 'man']

如何快速合并为一个字典?

答:字典推导式

字典推导式作用:快速合并列表为字典或提取字典中目标数据。

2.1 快速体验

1.创建一个字典:字典key是1-5数字,value是这个数字的2次方。

dict1 = {i: i**2 for i in range(1, 5)}
print(dict1)  # {1: 1, 2: 4, 3: 9, 4: 16}

2.将两个列表合并为一个字典

list1 = ['name', 'age', 'gender']
list2 = ['Tom', 20, 'man']
​
dict1 = {list1[i]: list2[i] for i in range(len(list1))}
print(dict1)

注意:如果两个数据个数相同,len统计任何一个列表的长度都可以。如果两个列表数据个数不同,len统计数据多的列表数据个数会报错;len统计数据少的列表数据个数不会报3

3.提取字典中目标数据

counts = {'MBP': 268, 'HP': 125, 'DELL': 201, 'Lenovo': 199, 'acer': 99}# 需求:提取上述电脑数量大于等于200的字典数据
count1 = {key: value for key, value in counts.items() if value >= 200}
print(count1)  # {'MBP': 268, 'DELL': 201}

筛选key-value对的条件要放到后面,这是语法上的要求。

三. 集合推导式

需求:创建一个集合,数据为下方列表的2次方。

3.1 快速体验

list1 = [1, 1, 2]

代码如下:

list1 = [1, 1, 2]
set1 = {i ** 2 for i in list1}
print(set1)  # {1, 4}

注意:集合有数据去重功能。

四、生成器

4.1 提供生成器的两种方式

python有两种方式提供生成器

  1. 生成器表达式生成器

  2. 函数生成器函数

4.1.1 列表推导式生成器表达式

new_list = [x * 3 for x in range(21)]
print(type(new_list))  # <class 'list'>

g = (x * 3 for x in range(21))
print(type(g))  # <class 'generator'>

4.1.2 调用生成器的方法

4.1.2.1 方式一 调用 next() 得到元素
g = (x * 3 for x in range(21))
print(type(g))  # <class 'generator'>
print(g.__next__())  # 0
print(g.__next__())  # 3
4.1.2.2 方式二 通过next()
g = (x * 3 for x in range(21))
print(type(g))  # <class 'generator'>
print(next(g))  # 0
print(next(g))  # 3

4.1.3 函数生成器

如果一个函数内部出现了yield这个关键字,那么该函数就是一个生成器函数,调用生成器函数将得到一个生成器,下面的示例将演示一个生成器函数的定义和使用:

# coding=utf-8
# 定义一个生成器函数
def my_generator(n):
    index = 0
    while index < n:
        yield index
        index += 1

generate = my_generator(5)
print(type(generate))
for i in generate:
    print(i)

程序输出结果为:

<class 'generator'>
0
1
2
3
4

理解上面的代码,要注意以下几点:

  • 当执行generate = my_generator(5) 这行代码时,my_generator函数里的代码没有被执行,因为函数里有yield关键,函数已经变成了一个生成器函数,生成器函数在被调用时会返回一个生成器,此时,函数里的代码不会被执行。

  • for循环的过程就是执行next方法的过程,当生成器的next方法被调用时,函数内部的代码才会执行,执行过程中遇到yield关键字,就会暂停(挂起),并将yield的参数做此次next方法的返回值。

  • 随着next方法的执行,函数内的while循环终究会有停止的时候,这个时候函数结束了,抛出StopIteration异常。

  • 生成器每次遇到yield暂停执行时,会把函数内的所有变量封存在生成器中,当下一次next方法被执行时,恢复这些变量。

  • 生成器函数内部,不允许使用return语句返回任何值,因为生成器函数已经默认返回一个生成器了,但是你可以只写一个return,后面不带任何值。

4.2 生成器有哪些优点

生成器是为延迟计算提供了支持,只在需要的时候再进行计算,这样一来,就能够减少对内存的使用。接下来将用一个例子来讲解生成器的好处。

已知有两个字典,内容如下:

dict_1 = {
    'key1': [1, 2, 3],
    'key2': [2, 3, 4],
    'key3': [3, 4, 5]
}

dict_2 = {
    'key1': ['a', 'b', 'c'],
    'key2': ['b', 'c', 'd'],
    'key3': ['c', 'd', 'e']
}

可以看到两个字典有相同的key,且value都是列表,由于篇幅有限,我让每个字典都只有3个key,但其实它可以拥有更多的key,value即列表里也可以拥有更多的元素,这样的假设是希望你能明白,更多的元素要占用更多的内存。

现在,我想知道两个字典合并后每个key对应的value是什么内容,并且对它进行输出,我先来实现一个耗费内存的版本,示例1 代码如下:

dict_1 = {
    'key1': [1, 2, 3],
    'key2': [2, 3, 4],
    'key3': [3, 4, 5]
}

dict_2 = {
    'key1': ['a', 'b', 'c'],
    'key2': ['b', 'c', 'd'],
    'key3': ['c', 'd', 'e']
}

def merge_dict(dict_1, dict_2):
    merge_dit = {}
    merge_dit.update(dict_1)
    for k, v in merge_dit.items():
        merge_dit[k].extend(dict_2[k])

    return merge_dit

merge_dit = merge_dict(dict_1, dict_2)
for k, v in merge_dit.items():
    print(k,v)

程序输出结果为:

key1 [1, 2, 3, 'a', 'b', 'c']
key2 [2, 3, 4, 'b', 'c', 'd']
key3 [3, 4, 5, 'c', 'd', 'e']

之所以说这个版本的实现是耗费内存的,是因为merge_dict函数创建了一个新的字典用来保存合并后的结果,理论上内存的使用增加了一倍,接下来看不节省内存的版本,示例2代码如下:

dict_1 = {
    'key1': [1, 2, 3],
    'key2': [2, 3, 4],
    'key3': [3, 4, 5]
}

dict_2 = {
    'key1': ['a', 'b', 'c'],
    'key2': ['b', 'c', 'd'],
    'key3': ['c', 'd', 'e']
}

for key, value in dict_1.items():
    value.extend(dict_2[key])
    print(key, value)

这个版本的实现的确在内存使用上比示例1好很多,但是却有一些不易被发现的缺陷

我现在的要求是输出合并后的key和value,但是如果出现了新的要求,计算value中的数字之和,那么就需要写一段新的代码,例如下面这样

for key, value in dict_1.items():
value.extend(dict_2[key])
sum = 0
for item in value:
    if isinstance(item, int):
        sum += item
print(key, sum)

对合并后结果的使用逻辑必须和合并的逻辑放在一起,就是说在for循环内部,既要完成字典合并又要完成结果的输出或者数字之和的计算

其他函数或者模块无法直接使用合并后的结果

对于第3条,如果采用示例1中的办法,其他函数可以直接使用新创建的字典,但这样耗费内存,采用示例2中的办法,虽然不耗费内存,但是必须直接对两个字典进行操作,而且还要关心如何去合并,那么有没有什么好的办法,既能不耗费内存,又能让代码工整简洁,利于维护呢,请看示例3

# coding=utf-8

dict_1 = {
    'key1': [1, 2, 3],
    'key2': [2, 3, 4],
    'key3': [3, 4, 5]
}

dict_2 = {
    'key1': ['a', 'b', 'c'],
    'key2': ['b', 'c', 'd'],
    'key3': ['c', 'd', 'e']
}

def gen_dict(dict_1, dict_2):
    """
    gen_dict 封装了合并两个字典的细节,而且不耗费内存
    :param dict_1:
    :param dict_2:
    :return:
    """
    for key, value in dict_1.items():
        value.extend(dict_2[key])
        yield key, value


# generate 是一个生成器,你在使用时根本不需要关心两个字典是如何合并的
generate_1 = gen_dict(dict_1, dict_2)
for key, value in generate_1:
    print(key, value)

# 计算合并后每个key所对应的value内部元素之和
generate_2 = gen_dict(dict_1, dict_2)
for key, value in generate_2:
    sum = 0
    for item in value:
        if isinstance(item, int):
            sum += item
    print(key, sum)

程序输出结果为:

('key3', [3, 4, 5, 'c', 'd', 'e'])
('key2', [2, 3, 4, 'b', 'c', 'd'])
('key1', [1, 2, 3, 'a', 'b', 'c'])
('key3', 12)
('key2', 9)
('key1', 6)

看到示例3的代码,是不是感到清爽许多,函数gen_dict返回一个生成器,该函数实现了对两个字典的合并而且不耗费内存。

在使用生成器generate_2时,你根本不需要关心两个字典是如何合并的,你只需要关心如何计算数字之和,合并字典的逻辑与合并后结果的处理逻辑是完全可以分开的,不必像示例2那样纠缠在一起。

4.3 使用生成器需要避免的坑

生成器虽然好用,但如果使用不当就会引发问题。

def generate_num(n):
    for i in range(n):
        yield n

iter_num = generate_num(10)
for num in iter_num:
    print(num)

for num in iter_num:        # 第二次执行for循环,没有任何效果
    print(num)

for循环的过程就是执行next方法的过程,经历过一次for循环以后,迭代器已经到了末尾,在for循环的内部,已经抛出StopIteration异常,for循环在捕捉到这个异常后停止遍历,因此第二次for循环时不会产生任何效果。

五、三个实例带你了解生成器的作用

python的生成器究竟有什么作用? 本文通过3个具体的例子向你阐述生成器的作用, 1. 使用生成器非常便利的结束两层for循环, 2. 一个for循环遍历多个列表, 3. 并行遍历多个可迭代对象

提到生成器,你可能会有一个简单的概念,如果函数里使用yield关键字,那么这个函数就是一个生成器,不同于return,生成器使用yield来返回值。

令人感到困惑之处在于,似乎yield和return 没啥区别,但实质上区别非常大,最明显之处便在于,return 语句执行后,函数就退出了,而yield语句执行时,仅仅是返回一个值而已,不存在函数结束这个概念,因此生成器都要结合for循环进行使用。

下面通过几个示例,来向你阐释生成器的作用

5.1 使用生成器非常便利的结束两层for循环

两层循环过程中,如果想通过break终止循环,是一件简单,但却很麻烦的事情,例如下面的这段代码

lst1 = [1, 2, 4, 5, 6]
lst2 = [1, 5, 6, 8, 9]

stop = False
for i in lst1:
    for j in lst2:
        if i + j == 10:
            stop = True
            break
    
    if stop:
        break

两层for循环的目的非常简单,从两个列表中各找出一个数,使得他们的和等于10,而且只需找出一个组合即可。

找到满足要求的组合后,为避免不必要的循环,需要终止循环,而此时,if条件语句在for循环的最里层,此处执行break,只能跳出最里层的for循环,想要终止最外层的for循环,就必须传递终止信号给它,代码里,通过stop=True,告知外层for循环可以终止了。

这样的代码写起来,显然有些繁琐,最后的if stop判断总显得多余,面对这种情况,可以巧妙的利用生成器来避免这种复杂的写法。

lst1 = [1, 2, 4, 5, 6]
lst2 = [1, 5, 6, 8, 9]


def num_generator(lst1, lst2):
    for i in lst1:
        for j in lst2:
            yield i, j


for i, j in num_generator(lst1, lst2):
    if i + j == 10:
        print(i, j)
        break

生成器num_generator里通过两层for循环对数据进行遍历,真正的业务逻辑使用一个for循环,这里就避免了跳出两层for循环的困境,这一次break,结束了for循环对num_generator的使用。

试想一下,如果没有生成器这种技术,有什么办法能实现一个break跳出两层for循环么? 这种技术还可以扩展到更多层的for循环。

5.2 chain–一个for循环遍历多个列表

有这样一种应用场景,你需要遍历多个列表来执行某个操作,比如下面两个列表,你需要找出列表里所有的偶数

lst1 = [1, 2, 4, 5, 6]
lst2 = [1, 5, 6, 8, 9]

5.2.1 嵌套循环

一种直接的方法是使用多层for循环

lst1 = [1, 2, 4, 5, 6]
lst2 = [1, 5, 6, 8, 9]

for lst in [lst1, lst2]:
    for item in lst:
        if item % 2 == 0:
            print(item)

5.2.2 创建新列表

为了减少for循环的层次,你也可以创建一个新的列表,包含这两个列表里的所有数据

lst1 = [1, 2, 4, 5, 6]
lst2 = [1, 5, 6, 8, 9]
lst = [*lst1, *lst2]

for item in lst:
    if item % 2 == 0:
        print(item)

5.2.3 chain

上面两个方法虽然都可以满足要求,但还有更简单的方法,使用chain

from itertools import chain

lst1 = [1, 2, 4, 5, 6]
lst2 = [1, 5, 6, 8, 9]

for item in chain(lst1, lst2):
    if item % 2 == 0:
        print(item)

chain内部会对传入的参数逐个进行遍历,lst1 和 lst2仿佛成为一个整体,如果不使用python提供的这个chain类,我们自己可以通过生成器来实现一个效果相同的函数

lst1 = [1, 2, 4, 5, 6]
lst2 = [1, 5, 6, 8, 9]


def my_chain(*iters):
    for iter_item in iters:
        for item in iter_item:
            yield item


for i in my_chain(lst1, lst2):
    if i % 2 == 0:
        print(i)

5.3 zip–并行遍历多个可迭代对象

使用chain时,是对列表逐个进行遍历,但有时,我们又希望是并行遍历,python原生的zip函数提供了这样的功能,下面是使用示例

lst1 = ['python', 'java', 'c']
lst2 = [95, 97, 98]

dic = {}
for language, score in zip(lst1, lst2):
    dic[language] = score

print(dic)

两个列表,第一个列表是科目,第二个列表是分数,需要将这两个列表的内容转换为字典,使用zip函数,就可以并行遍历两个列表,接下来,我们自己使用生成器来实现一个相同功能的函数

lst1 = ['python', 'java', 'c']
lst2 = [95, 97, 98]


def my_zip(*args):
    min_len = min(len(item) for item in args)
    index = 0
    while index < min_len:
        lst = []
        for iter_item in args:
            lst.append(iter_item[index])

        index += 1
        yield tuple(lst)


info = {}
for language, score in my_zip(lst1, lst2):
    info[language] = score

六、迭代器

迭代器是一个让程序员可以遍历一个容器的对象,这里有三个概念需要你深刻的理解

  1. 可迭代对象(Iterable)

  2. 迭代器(Iterator)

  3. 迭代(Iteration)

6.1 可迭代对象

6.1.1 直观理解

list,tuple,dict, set, str,还有生成器这样的对象,可以直接作用于for循环,这些都是可迭代对象,一个打开的file对象和socket对象也是可迭代对象。

下面的代码演示for循环如何作用于一个可迭代对象

lst = [1, 2, 3]
for item in lst:
    print(item)

程序执行结果为:

1
2
3
  • isinstance() 检查一个对象是否是另一个对象的实例

下面的代码,演示了如何判断一个对象是否为可迭代对象

from collections import Iterable
lst = [1, 2, 3]
print(isinstance(lst, Iterable))  # Ture

6.2 迭代器(Iterator)

6.2.1 什么是迭代器

迭代器是访问可迭代对象的工具

  1. 迭代器是指用iter(obj)函数返回的对象(实例)

  2. 实现了iter方法的对象就是可迭代对象

  3. 迭代器是指用next(it)函数获取可迭代对象的数据,

  4. 实现了 iter方法且又实现了next方法的对象就是迭代器

如果一个python对象实现 实现了iter方法的对象就是可迭代对象和 实现了 iter方法且又实现了被next()函数调用,并不断返回下一个值的对象称之为迭代器

所谓迭代,就是不停的调用迭代器的next方法的过程,直到抛出StopIteration异常

6.2.1 list 是迭代器么

list不是迭代器,因为list这个数据类型没有实现next方法

from collections import Iterable, Iterator
lst = [1, 2, 3]
# lst列表是可迭代对象
print(isinstance(lst, Iterable))  # True

# Iterator检查lst列表是不是迭代器
print(isinstance(lst, Iterator))  # False  lst列表不是迭代器,因为它没有实现next方法

# iter方法会返回一个迭代器
lst_iter = iter(lst)
print(isinstance(lst_iter, Iterator))  # True

list内部实现了__getitem__, 因此内置函数iter检测到list没有实现next方法但是实现了__getitem__方法后,会创建一个迭代器,并尝试通过索引(从0开始)来获取元素。

6.2.2 迭代器不能重复使用

一个迭代器是不能重复使用的,因为迭代的本意就是从头到尾遍历某个集合,如果你已经遍历到末尾,此时想重新使用这个迭代器从到到尾来遍历就是不可行的,next方法已经走到了末尾。
示例1:

lst = [1, 2, 3]
iter_lst = iter(lst)
for i in iter_lst:
    print(i)

# 这一次没有任何数据输出,因为iter_lst 作为一个迭代器,已经到达末尾
# 调用next方法只会抛出异常,被for循环捕获
for i in iter_lst:
    print(i)

程序输出结果为:

1
2
3

上面的示例中,iter_lst是iter函数返回的迭代器,使用了一次之后,第二次再使用就没有任何效果了,你可能会感到困惑,并指出下面的代码有些难以理解。
示例2:

lst = [1, 2, 3]
for i in lst:
    print(i)

# 两次都可以输出
for i in lst:
    print(i)

程序输出结果为:

1
2
3
1
2
3

如果你对这两段代码的输出结果感到困惑,那么你需要细细的品味理解下面的内容:

示例1 中,for循环作用于iter_lst,而iter_lst 是一个迭代器,两次for循环使用的是同一个迭代器,但迭代器只能使用一次

示例2 中,for循环作用于lst,lst是一个列表,此时,python内部使用iter函数返回lst的迭代器供for循环使用,两次for循环使用的是不同的迭代器,因此两个for循环均有输出

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值