Python中列表推导和生成器表达式有多强大?

文 | 菊子皮(转载请注明出处)

关注公众号:AIAS编程有道

同名B站:AIAS编程有道

环境:Python 3.7,VS Code

主要参考:《流畅的Python》

列表推导及其可读性

写程序那肯定是稳中求快,列表推导就是构建列表list的一种快捷方式,而生成器表达式则可用来创建其他类型的序列。如果你经常使用for循环生成一个列表,那就太low了,并且代码的可读性以及程序的高效性也难以保证。为啥这么说呢,不妨看一个小例子。

案例目标:把一个字符串编程Unicode码位的列表

新手程序员会这么写:

symbols = '$¢£¥€¤'
codes = []
for symbol in symbols:
    codes.append(ord(symbol))
print(codes)  # [36, 162, 163, 165, 8364, 164]

新手程序员会问,不这么写还有什么写法?那就用到了列表推导和生成器表达式了,如下:

symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]
print(codes)   # [36, 162, 163, 165, 8364, 164]

为了生成codes第二种写法代码简短,清晰明了,也不难理解。

拓展: 知之为知之,不知为不知,是知也

  • ord() 函数是 chr() 函数(对于 8 位的 ASCII 字符串,用一个整数作参数,返回一个对应的字符)的配对函数,它以一个字符串(Unicode 字符)作为参数,返回对应的 ASCII 数值,或者 Unicode 数值。
  • Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

当然for循环也不是一无是处,也能胜任很多任务:遍历一个序列以求得总数或挑出某个特定的元素、用来计算总和或是平均数,还有其他任何你想做的事情等。

建议: 使用列表推导通常的原则是,只用列表推导来创建新的列表,并且尽量保持简短。

tips: Python会忽略代码里[]、{}和( )中的换行,因此如果你的代码里有多行的列表、列表推导、生成器表达式、字典这一类的,可以省略不太好看的续行符\。

列表推导同filter和map的比较

filter和map合起来能做的事情,列表推导也可以做,而且还不需要借助难以理解和阅读的lambda表达式

拓展:Python3内建函数

  • filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回一个迭代器对象,如果要转换为列表,可以使用 list() 来转换。该方法接收两个参数,第一个为函数,第二个为序列,序列的每个元素作为参数传递给函数进行判判断,然后返回 True 或 False,最后将返回 True 的元素放到新列表中。
  • map() 会根据提供的函数对指定序列做映射。第一个参数 function 以参数序列中的每一个元素调用 function 函数,返回包含每次 function 函数返回值的新列表。

例:使用列表推导和map/filter组合选取Unicode > 127 的符号

symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]
print(codes)   # [36, 162, 163, 165, 8364, 164]
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
print(beyond_ascii)  # [162, 163, 165, 8364, 164]
beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
print(beyond_ascii)  # [162, 163, 165, 8364, 164]

list(filter(lambda c: c > 127, map(ord, symbols))) 说明 这行代码首先是使用map函数按照ord函数将symbols符号转为Unicode对应的数字返回一个可迭代的对象,然后filter 对上一步骤的结果按照一个匿名函数lambda c : c> 127 (含义:判断参数是否大于127, 大于的话就返回)得到过滤后的结果,最后再将其转为一个list。

而这两种方法的速度又是怎么样的呢?我们看看下面速度比较的程序:

import timeit

TIMES = 10000

SETUP = """
symbols = '$¢£¥€¤'
def non_ascii(c):
    return c > 127
"""

def clock(label, cmd):
    res = timeit.repeat(cmd, setup=SETUP, number=TIMES)
    print(label, *('{:.3f}'.format(x) for x in res))

# <1>
clock('listcomp        :', '[ord(s) for s in symbols if ord(s) > 127]')
# listcomp        : 0.017 0.017 0.016 0.016 0.015
# <2>
clock('listcomp + func :', '[ord(s) for s in symbols if non_ascii(ord(s))]')
# listcomp + func : 0.026 0.023 0.025 0.021 0.023
# <3>
clock('filter + lambda :', 'list(filter(lambda c: c > 127, map(ord, symbols)))')
# filter + lambda : 0.026 0.021 0.020 0.021 0.019
# <4>
clock('filter + func   :', 'list(filter(non_ascii, map(ord, symbols)))')
# filter + func   : 0.026 0.022 0.019 0.019 0.019

程序说明:(默认每次执行语句10000次)

<1> 使用列表推导情况,重复运行了5组 得到的运行时间

<2> 使用列表推导以及相关函数情况(在setup定义的函数,供cmd,也就是state这个复数调用)

<3> 使用filter、map、lambda情况

<4> 使用filter、map和自定义函数情况

根据结果可以看出,列表推导运行更快。

笛卡儿积

两个或以上的列表中的元素对构成元组,这些元组构成的列表就是笛卡儿积,列表里的元素是由输入的可迭代类型的元素对构成的元组,因此笛卡儿积列表的长度等于输入变量的长度的乘积,可参考下图理解:

在这里插入图片描述

现在我们用列表推导来计算一个笛卡儿积(一个列表)。

程序描述: 需要一个列表,列表里是3种不同尺寸的T恤衫,每个尺寸都有2个颜色,那么列表里有6种组合。(当然for循坏也是可以的,这里使用列表推导)

colors = ['black', 'white']
sizes = ['S', 'M', 'L']
tshirts = [(color, size) for color in colors for size in sizes] # <1> 
print(tshirts)  # [('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')]
tshirts = [(color, size) for size in sizes for color in colors] # <2>
print(tshirts)  # [('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'), ('black', 'L'), ('white', 'L')]

代码说明:

<1> 结果是先以颜色排列,再以尺码排列

<2> 结果是先以尺码排列,再以颜色排列

生成器表达式

列表推导的作用只有一个:生成列表。如果想生成其他类型的序列,生成器表达式就派上了用场。比较重要的一点是:它遵守迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里,那么这种方式更省内存

生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。

使用案例: 用生成器表达式初始化元组和数组

symbols = '$¢£¥€¤'
print(tuple(ord(s) for s in symbols))  # <1> (36, 162, 163, 165, 8364, 164)
import array
print(array.array('I', (ord(s) for s in symbols)))  # <2> array('I', [36, 162, 163, 165, 8364, 164])

代码说明:

<1> 如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。因为生成器最后生成的是一个迭代器结果,打印的话是一个地址,需在最后使用tuple进行类型转换。

<2> array.array 这是Python中的原生数组类型,底层使用C实现,后文还会介绍。它创建的方式是两个参数,第一个是指定数组类型中元素的类型,也就是存储方式,例如"I”就表示unsigned int,在底层存储就是一个数就占用2个字节。另外很多Python认为列表就是数组,在Python中严格来说是不合适的,列表可以充当数组使用,但是其效率远没有array.array高,其中NumPy效率也是非常高的,在真正意义上可以说是数组。

生成器的结果是一个迭代器结果,我们就可以用for来进行遍历,再根据自己的想法使用迭代器中的元素,使用for循环使用迭代器结果如下,需要说明的是,生成器表达式会在每次for循环运行时才生成一个组合。

colors = ['black', 'white']
sizes = ['S', 'M', 'L']
for tshirt in ('%s%s' % (c, s) for c in colors for s in sizes):
    # 使用
    print(tshirt)

我的订阅号
发布了150 篇原创文章 · 获赞 220 · 访问量 39万+

猜你喜欢

转载自blog.csdn.net/meiqi0538/article/details/104324097