Python中的三个”黑魔法“与”骚操作“

2,712 阅读9分钟

本文主要介绍Python的高级特性:列表推导式、迭代器和生成器,是面试中经常会被问到的特性。因为生成器实现了迭代器协议,可由列表推导式来生成,所有,这三个概念作为一章来介绍,是最便于大家理解的,现在看不懂没关系,下面我不仅是会让大家知其然,重要的更是要知其所以然。

列表推导式

前几天有个HR让我谈谈列表推导式,我说这我经常用,就是用旧的列表生成一个新的列表的公式,他直接就把我拒了,让我回去复习一下,挺受打击的,所以决定也帮助大家回顾一下。

内容

  • 列表推导式:旧的列表->新的列表
  • 了解:字典推导式 集合推导式

1.列表推导式:

格式 [表达式 for 变量 in 旧列表]
[表达式 for 变量 in 旧列表 if 条件]
例1:生成名字长度大于3且首字母大写的新列表。

names_old = ['tom', 'amy', 'daming', 'lingling']
names_new = [name.capitalize() for name in names_old if len(name) > 3]
print(names_new)

输出:

['Daming', 'Lingling']

例2: (大厂初级笔试题目)生成一个元组列表,要求每个元素为(0-5偶数, 0-10奇数)形式。输出结果为:

[(0, 1), (0, 3), (0, 5), (0, 7), (0, 9), (2, 1), (2, 3), (2, 5), (2, 7), (2, 9), (4, 1), (4, 3), (4, 5), (4, 7), (4, 9)]

for循环实现代码:

new_list = list()
for i in range(5):  # 偶数
    if i % 2 == 0:
        for j in range(10):  # 奇数
            if j % 2 != 0:
                new_list.append((i, j))

列表推导式代码:

new_list = [(i, j) for i in range(5) for j in range(10) if i % 2 == 0 and j % 2 != 0]

例3:(大厂初级笔试题目)给出一个员工列表:

employees_old = [{'name': "tmo", "salary": 4800},
                 {'name': "amy", "salary": 3800},
                 {'name': "daming", "salary": 7000},
                 {'name': "lingling", "salary": 5600}] 

如果员工薪资大于5000则加200,否则加500,输出新的员工列表。
列表推导式:

employees_new = [employee['salary'] + 200 if employee['salary'] > 5000 else employee['salary'] + 500 for employee in employees_old]
print(employees_new)

输出:

[5300, 4300, 7200, 5800]

发现结果是员工薪资列表,回过头看一下代码,确实是把得到的数字给了列表,那要返回员工列表要怎么实现呢?让我们用普通for循环的方式来进行一下对比:

for employee in employees_old:
    if employee['salary'] > 5000:
        employee['salary'] += 200
    else:
        employee['salary'] += 500

print(employees_old)

输出:

[{'name': 'tmo', 'salary': 5300}, {'name': 'amy', 'salary': 4300}, {'name': 'daming', 'salary': 7200}, {'name': 'lingling', 'salary': 5800}]

没错,我们注意到两者的差别了,列表推导式我们少了一步赋值(在字典元素上进行赋值),不能直接返回一个薪资数值而是一个员工字典给列表。正确的列表推导式如下:

employees_new = [
    {'name': employee['name'], 'salary': employee['salary'] + 200} if employee['salary'] > 5000 else
    {'name': employee['name'], 'salary': employee['salary'] + 500} for employee in employees_old]

print(employees_new)

2.字典推导式:

例1:

dict_old = {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'C'}
dict_new = {value: key for key, value in dict_old.items()}
print(dict_new)

输出:

{'A': 'a', 'B': 'b', 'C': 'd'}

3.集合推导式:

类似列表推导式 典型用法:去重
例1:

list_old = [1, 2, 3, 5, 2, 3]
set_new = {x for x in list_old}
print(set_new)

输出:

{1, 2 ,3, 5}

小结:

到目前为止,列表推导式不就是一个用来创建列表的式子么?除了可以简化代码,装装X?其实,列表推导式还有另一个优点是相比于for循环更高效,因为列表推导式在执行时调用的是Python的底层C代码,而for循环则是用Python代码来执行。嗷~面试官最想听到的,是第二点。

迭代器

由于迭代器协议对很多人来说,是一个较为抽象的概念,而且生成器自动实现了迭代器协议,所以我们需要先讲解一下迭代器协议的概念,也是为了更好的理解接下来的生成器。

概念

可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator
迭代是访问集合元素的一种方式,迭代器是一个可以记住遍历位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有元素被访问完结束。
迭代器只能往前不能后退。

  • 迭代器协议:是指对象需要提供__next__()方法,它要么返回迭代中的下一项,要么就引起一个StopIteration异常,以终止迭代。
  • 可迭代对象:就是实现了迭代器协议的对象。

举个例子,对Python稍微熟悉一点的朋友应该知道,Python的for循环不但可以用来遍历list,还可以用来遍历文件对象,如下所示:

with open('F:/test/test.txt') as f:
    for line in f:
        print(line)

为什么在Python中,文件还可以使用for循环进行遍历呢?这是因为,在Python中,文件对象实现了迭代器协议,for循环并不知道它遍历的是一个文件对象,它只管使用迭代器协议访问对象即可。正是由于Python的文件对象实现了迭代器协议,我们才得以使用如此方便的方式访问文件,如下所示:

with open('F:/test/test.txt') as f:
    print(dir(f))

输出:

['__class__', '__del__', '__dict__', '__dir__', '__init__', '__iter__', '__next__', 'closed', 'line_buffering', 'newlines', 'read', 'readline'......]     

可迭代的是不是肯定就是迭代器?

  • 生成器是可迭代的,也是迭代器。
  • list是可迭代的,但不是迭代器。list可以借助iter()函数将可迭代的变成迭代器list->iter(list)->迭代器next()
    借助iter()生成迭代器

可迭代对象:

  1. 生成器
  2. 元组 列表 集合 字典 字符串

如何判断一个对象是否是可迭代?

借助isinstance()函数:

from collections import Iterable

print(isinstance([x for x in range(10)], Iterable))  # 列表
print(isinstance('hello world', Iterable))  # 字符串
print(isinstance(100, Iterable))  # 数字
print(isinstance((x for x in range(10)), Iterable))  # 迭代器

输出:

True
True
False
True

生成器

生成器是Python最有用的特性之一,也是使用的最不广泛的Python特性之一。究其原因,主要是因为,在其他主流语言里面没有生成器的概念。正是由于生成器是一个“新”的东西,所以,它一方面没有引起广大工程师的重视,另一方面,也增加了工程师的学习成本,最终导致大家错过了Python中如此有用的一个特性。

概念

我们已经知道,通过列表推导式可以直接创建一个列表,但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面那几个元素,那后面绝大多数元素占用的空间都白白浪费了。所以,如果列表元素可以按照某种算法在循环的过程中不断推算出后续的元素,这样既不必创建完整的list,从而还可以节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator
Python使用生成器对延迟操作提供了支持。所谓延迟操作,是指在需要的时候才产生结果,而不是立即产生结果。这也是生成器的主要好处。

定义生成器

Python有两种不同的方式提供生成器:

方法一:借助列表推导式

生成器表达式:类似于列表推导(这也就是为什么第一节我要先介绍列表推导式),但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表。
例1:

my_generator = (x for x in range(5))  # 注意是()不是[]
print(my_generator)  # 发现不能打印出元素
print(type(my_generator))
print(my_generator.__next__())  # 三种得到元素的方法,注意看输出结果
print(next(my_generator))
for i in my_generator:
    print(i)

# 注意会抛出StopIteration异常
# print(next(my_generator))  
print(next(my_generator))   # generator只能遍历一次

输出:

Traceback (most recent call last):
  File "E:/pycharm/Leetcode/RL_Learning/printdata.py", line 11, in <module>
    print(next(my_generator))
StopIteration
<generator object <genexpr> at 0x0000000000513660>
<class 'generator'>
0
1
2
3
4

方法二:借助函数

生成器函数:使用yield语句而不是return语句返回函数结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,起到暂停的作用,以便下次从它离开的地方继续执行。

步骤:

  1. 定义函数,函数返回使用yield关键字;
  2. 调用函数,接收函数返回值;
  3. 得到的返回结果就是生成器;
  4. 借助next()__nest__()得到想要的元素。

例2:你的函数里面只要出现了yield关键字,你的函数就不再是函数了,就变成生成器了:

# 斐波那契数列:
def fib(length):    # 1. 定义函数
    a, b = 0, 1
    n = 0
    while n < length:
        n += 1
        yield b         # return b + 暂停
        a, b = b, a + b


g = fib(5)     # 2. 调用函数
print(g)     # 3. 返回的就是生成器
print(next(g))     # 4. 借助`next()`或`__nest__()`得到想要的元素
print(next(g))    # 每调用一次产生一个值
print(next(g))
print(g.__next__())
print(g.__next__())

输出:

<generator object fib at 0x0000000001DDDFC0>
1
1
2
3
5

注意:生成器只能遍历一次。
当调用函数的时候,并没有进函数进行执行,而是直接生成一个生成器,当调用next的时候,才进入函数真正开始执行,除了第一次调用next()方法是从函数头开始执行,其余每次都是接着从上次执行到yield的地方接着执行的。

小结:

使用生成器以后,代码行数更少。大家要记住,如果想把代码写的Pythonic,在保证代码可读性的前提下,代码行数越少越好。
合理使用生成器,能够有效提高代码可读性。只要大家完全接受了生成器的概念,理解了yield语句和return语句一样,也是返回一个值。那么,就能够理解为什么使用生成器比不使用生成器要好,能够理解使用生成器真的可以让代码变得清晰易懂。
在实际工作中,充分利用Python生成器,不但能够减少内存使用,还能够提高代码可读性。掌握生成器也是Python高手的标配。 如果本文对你有帮助,不要忘记关注点赞或收藏支持一下~

更多干货(必备技能):
从原理到实战,彻底搞懂Nginx
从原理到实战,彻底搞懂Nginx(高级篇)
Kafka概述,深入理解架构