3-5-解析和生成

1. 列表解析和函数式编程工具

列表解析能够更加通用,可以在一个列表解析中编写任意数量的嵌套的 for 循环,并且每一个都有可选的关联的 if 测试:

1
2
3
[expression for target1 in iterable1 [if condition1]
for target2 in iterable2 [if condition2] ...
for targetN in iterableN [if conditionN] ]

当 for 分句嵌套在列表解析中时,就像等效的嵌套的 for 循环语句。

这种相同的语法继承自集合和字典解析式以及生成器表达式。

1
2
[x + y for x in [0, 1, 2] for y in [100, 200, 300]]

[100, 200, 300, 101, 201, 301, 102, 202, 302]
1
[x + y for x in 'spam' if x in 'sm' for y in 'SPAM' if y in ('P', 'A')]
['sP', 'sA', 'mP', 'mA']
1
2
3
[x + y + z for x in 'spam' if x in 'sm'
for y in 'SPAM' if y in ('P', 'A')
for z in '123' if z > '1']
['sP2', 'sP3', 'sA2', 'sA3', 'mP2', 'mP3', 'mA2', 'mA3']
1
[(x, y) for x in range(5) if x % 2 == 0 for y in range(5) if y % 2 == 1]
[(0, 1), (0, 3), (2, 1), (2, 3), (4, 1), (4, 3)]

1.1 列表解析和矩阵

使用 Python 编写矩阵的一个基本方法就是使用嵌套的列表结构:

1
2
3
4
5
6
7
8
9
M = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]

N = [[2, 2, 2],
[3, 3, 3],
[4, 4, 4]]

M[1] # 第二行
[4, 5, 6]
1
M[1][2]                             # 第二行,第三列
6
1
[row[1] for row in M]               # 第二列
[2, 5, 8]
1
[M[row][1] for row in (0, 1, 2)]    # 使用偏移
[2, 5, 8]
1
[M[i][i] for i in range(len(M))]    # 对角线元素
[1, 5, 9]
1
[M[i][len(M)-1-i] for i in range(len(M))]
[3, 5, 7]
1
[col + 10 for row in M for col in row]
[11, 12, 13, 14, 15, 16, 17, 18, 19]
1
[[col + 10 for col in row] for row in M]
[[11, 12, 13], [14, 15, 16], [17, 18, 19]]

列表解析可以混合多个矩阵。

1
[M[row][col] * N[row][col] for row in range(3) for col in range(3)]  # 矩阵对元素的乘积,单层列表
[2, 4, 6, 12, 15, 18, 28, 32, 36]
1
[[M[row][col] * N[row][col] for col in range(3)] for row in range(3)]   # 嵌套列表结构
[[2, 4, 6], [12, 15, 18], [28, 32, 36]]
1
[[col1 * col2 for (col1, col2) in zip(row1, row2)] for (row1, row2) in zip(M, N)]  # 使用 zip 组合要相乘的项
[[2, 4, 6], [12, 15, 18], [28, 32, 36]]

2. 生成器函数和表达式

Python 提供了工具在需要的时候才产生结果,而不是立即产生结果。有两种语言结构尽可能地延迟结果创建。

  • 生成器函数:编写为常规的 def 语句,但是使用 yield 语句一次返回一个结果,在每个结果之间挂起和继续它们的状态。
  • 生成器表达式:类似于列表解析,但是它们返回按需产生结果的一个对象,而不是构建一个结果列表。

由于二者都不会一次性构建一个列表,它们节省了内存空间,而且允许计算时间分散到各个结果请求。

2.1 生成器函数:yield VS return

生成器函数,编写可以送回一个值并随后从其退出的地方继续的函数,因为它们随着时间产生值的一个序列。

一般来说,生成器函数和常规函数一样,然而,当创建时,生成器函数自动实现迭代协议,以便可以出现在迭代背景中。

状态挂起
生成器函数自动在生成值的时候挂起并继续函数的执行。由于生成器函数在挂起时保存的状态包含它们的整个本地作用域,当函数恢复时,它们的本地变量保持了信息并且使其可用。

yield 语句挂起该函数并向调用者发送回一个值,但是,保留足够的状态以使得函数能够从它离开的地方继续。当继续时,函数在上一个 yield 返回后立即继续执行。这允许代码随着时间产生一系列的值,而不是一次计算它们。

迭代协议整合
可迭代对象定义了一个 __next__ 方法,它返回迭代中的下一项,或者引发一个特殊的 StopIteration 异常来终止迭代。一个对象的迭代器用 iter 内置函数接收。

如果支持该协议,Python 的 for 循环以及其他的迭代环境,使用这种迭代协议来遍历一个序列或值生成器;如果不支持,迭代返回去重复索引序列。

要支持这一协议,函数包含一条 yield 语句,该语句特别编译为生成器。当调用时,它们返回一个生成器对象,该对象支持一个名为 __next__ 的自动创建的方法来开始或继续执行的接口。

生成器函数也可能有一条 return 语句,总是在 def 语句块的末尾,直接终止值的生成。从技术上讲,可以在任何常规函数退出执行后,引发一个 StopIteration 异常来实现。从调用者的角度来看,生成器的 __next__ 方法继续函数并且运行到下一个 yield 结果返回或引发一个 StopIteration 异常。

生成器函数应用

1
2
3
4
# 生成一系列的数字的平方
def gensquares(N):
for i in range(N):
yield i ** 2

函数在每次循环时都会产生一个值,之后将其返还给它的调用者。当它被暂停后,它的上一个状态保存了下来,并且在 yield 语句之后控制器马上被回收。

1
2
for i in gensquares(5):
print(i, end=' : ') # 打印最后生成的值
0 : 1 : 4 : 9 : 16 : 

为了终止生成值,函数可以使用一个无值的 return 语句,或者在函数主体最后简单地让控制器脱离。

看看在 for 里面发生了什么。

1
2
x = gensquares(4)
x
<generator object gensquares at 0x00000155CCB699E8>
1
next(x)
0
1
next(x)
1
1
next(x)
4
1
next(x)
9
1
next(x)
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-8-5e4e57af3a97> in <module>()
----> 1 next(x)

StopIteration: 

注意迭代协议中顶层的 iter 调用在这里不需要,因为生成器是自己的迭代器,仅支持一个活跃的迭代扫描。

1
2
y = gensquares(5)
iter(y) is y
True
1
next(y)
0

为什么使用生成器函数?
我们能够简单地一次就构建一个所获得的值的列表。

1
2
3
4
5
6
7
def buildsquares(n):
res = []
for i in range(n): res.append(i ** 2)
return res

for x in buildsquares(5):
print(x, end=' : ')
0 : 1 : 4 : 9 : 16 : 

还能使用 for 循环、map 或者列表解析来实现:

1
2
for x in [n ** 2 for n in range(5)]:
print(x, end=' : ')
0 : 1 : 4 : 9 : 16 : 
1
2
for x in map((lambda n: n ** 2), range(5)):
print(x, end=' : ')
0 : 1 : 4 : 9 : 16 : 

尽管如此,生成器在内存使用和性能方面都更好。它们允许函数避免临时再做所有的工作,当结果的列表很大或者在处理每一个结果都需要很多时间时,这一点尤其有用。生成器将在循环迭代中处理一系列值的时间分布开来。

对于更多高级的应用,它们提供了一个更简单的替代方案来手动保存类的对象的迭代中的状态。有了生成器,函数变量就能进行自动的保存和恢复。

生成器函数能够在任意类型的对象上进行操作和返回,包括元组调用,enumeration 和字典解析:

1
2
3
4
5
def ups(line):
for sub in line.split(','): # 子字符串生成器
yield sub.upper()

tuple(ups('aaa,bbb,ccc'))
('AAA', 'BBB', 'CCC')
1
{i: s for (i, s) in enumerate(ups('aaa,bbb,ccc'))}
{0: 'AAA', 1: 'BBB', 2: 'CCC'}

2.2 生成器表达式:迭代器遇到列表解析

生成器表达式就像一般的列表解析一样,但是它们是括在圆括号中而不是方括号中。

1
(x ** 2 for x in range(4))         
<generator object <genexpr> at 0x000001ACF4CD9BF8>

生成器表达式不是在内存中构建结果,而是返回一个生成器对象,这个对象将会支持迭代协议。

1
G = (x ** 2 for x in range(4))
1
next(G)
0
1
next(G)
1
1
next(G)
4
1
next(G)
9
1
next(G)
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-8-380e167d6934> in <module>()
----> 1 next(G)

StopIteration: 

作为可迭代的,生成器表达式可以出现在任意迭代环境中。

1
2
# 字符串 join 方法中使用生成器表达式
''.join(x.upper() for x in 'aaa,bbb,ccc'.split(','))
'AAABBBCCC'
1
2
3
# 元组赋值中使用生成器表达式
a, b, c = (x + '\n' for x in 'aaa,bbb,ccc'.split(','))
a, c
('aaa\n', 'ccc\n')

如果生成器表达式是在其他的括号之内,就像在那些函数调用之中,在这种情况下,生成器自身的括号就不是必须的了。

1
sum(x ** 2 for x in range(4))
14
1
sorted(x ** 2 for x in range(4))
[0, 1, 4, 9]
1
sorted((x ** 2 for x in range(4)), reverse=True)    # 还是需要额外的括号
[9, 4, 1, 0]

为什么使用生成器表达式?
生成器表达式是对内存空间的一个优化,它们不需要像列表解析一样一次构造出整个列表结果。

生成器表达式可能比列表解析运行得稍慢,因此,它们最好仅在非常大的结果集合或者不能等待所有结果生成的应用中使用。

生成器表达式 VS map
生成器表达式经常和 map 调用是等效的,因为两者都是按需求生成结果。

1
list(map(abs, (-1, -2, 3, 4)))
[1, 2, 3, 4]
1
list(abs(x) for x in (-1, -2, 3, 4))
[1, 2, 3, 4]
1
list(map(lambda x: x * 2, (1, 2, 3, 4)))    # 非函数调用时,生成器表达式可能更简单
[2, 4, 6, 8]
1
list(x * 2 for x in (1, 2, 3, 4))
[2, 4, 6, 8]

在文字处理中,列表解析生成一个额外的临时结果列表,这完全没有意义。当操作不是一个调用时,map 相比于生成器表达式语法,丢失了简便性这一要点:

1
2
line = 'aaa,bbb,ccc'
''.join([x.upper() for x in line.split(',')]) # 生成无意义的列表
'AAABBBCCC'
1
''.join(x.upper() for x in line.split(','))
'AAABBBCCC'
1
''.join(map(str.upper, line.split(',')))
'AAABBBCCC'
1
''.join(x * 2 for x in line.split(','))           # 生成器表达式更简单
'aaaaaabbbbbbcccccc'
1
''.join(map(lambda x: x * 2, line.split(',')))
'aaaaaabbbbbbcccccc'

map 和生成器表达式都可以任意嵌套,需要一个 list 调用或者其他迭代环境来开始生成结果的进程。

1
[x * 2 for x in [abs(x) for x in (-1, -2, 3, 4)]]  # 列表解析需要生成两个列表
[2, 4, 6, 8]
1
list(map(lambda x:x * 2, map(abs, (-1, -2, 3, 4))))
[2, 4, 6, 8]
1
list(x * 2 for x in (abs(x) for x in (-1, -2, 3, 4)))
[2, 4, 6, 8]

非嵌套方法可以提供更简洁的解决方法,且仍然利用了生成器的优点:

1
list(abs(x) * 2 for x in (-1, -2, 3, 4))
[2, 4, 6, 8]

生成器表达式 VS filter
生成器表达式支持所有常规的列表解析语法——包括 if 分句。使用 if 分句的生成器表达式在操作上和 filter 等效:

1
2
line = 'aa bbb c'
''.join(x for x in line.split() if len(x) > 1)
'aabbb'
1
''.join(filter(lambda x: len(x) > 1, line.split()))
'aabbb'

至于列表解析,在 filter 结果上添加处理过程也需要 map,这使得 filter 比生成器表达式更加复杂:

1
''.join(x.upper() for x in line.split() if len(x) > 1)
'AABBB'
1
''.join(map(str.upper, filter(lambda x: len(x) > 1, line.split())))
'AABBB'

2.3 生成器是单迭代器对象

生成器函数和生成器表达式自身都是迭代器,并由此只支持一次活跃迭代。

1
2
G = (c * 4 for c in 'SPAM')
iter(G) is G
True

如果手动使用多迭代器来迭代结果流,它们将会指向相同的位置。一旦任何迭代器运行到完成,所有的迭代器都将用尽,必须产生一个新的生成器以再次开始。

1
2
I1 = iter(G)
next(I1)
'SSSS'
1
next(I1)
'PPPP'
1
I2 = iter(G)
1
next(I2)
'AAAA'
1
next(I2)
'MMMM'
1
I3 = iter(G)
1
next(I3)
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-40-6626ccf10866> in <module>()
----> 1 next(I3)

StopIteration: 

yield from Extension

Python 3.3 介绍了 yield 语句的扩展语法,允许使用 from generator 分句代表子生成器。简单情况下,它相当于一个 yield 的 for 循环。

1
2
3
4
5
def both(N):
for i in range(N): yield i
for i in (x ** 2 for x in range(N)): yield i

list(both(5))
[0, 1, 2, 3, 4, 0, 1, 4, 9, 16]
1
2
3
4
5
def both(N):                   # 新的语法更简单明确
yield from range(N)
yield from (x ** 2 for x in range(N))

list(both(5))
[0, 1, 2, 3, 4, 0, 1, 4, 9, 16]

2.4 示例

示例只在序列中有效。

打乱序列
传统分片和拼接可以将序列重新排列:

1
2
3
4
L, S = [1, 2, 3], 'spam'
for i in range(len(S)):
S = S[1:] + S[:1] # 将前面的元素移至末尾
print(S, end=' ')
pams amsp mspa spam 
1
2
3
for i in range(len(S)):
X = S[i:] + S[:i] # 后面部分 + 前面部分
print(X, end=' ')
spam pams amsp mspa 

生成器函数
生成器函数在活动时保留了本地作用域状态,最小化内存空间需要,将工作分为更短的时间片。

1
2
3
4
5
6
def scramble(seq):
for i in range(len(seq)):
seq = seq[1:] + seq[:1]
yield seq

list(scramble('spam'))
['pams', 'amsp', 'mspa', 'spam']
1
list(scramble((1, 2, 3)))
[(2, 3, 1), (3, 1, 2), (1, 2, 3)]

生成器表达式
表达式不如完整函数灵活,但是它们能自动生成值,因此通常更简洁。

1
2
3
S = 'spam'
G = (S[i:] + S[:i] for i in range(len(S)))
list(G)
['spam', 'pams', 'amsp', 'mspa']
1
2
F = lambda seq: (seq[i:] + seq[:i] for i in range(len(seq)))
list(F(S))
['spam', 'pams', 'amsp', 'mspa']

2.5 示例:用迭代工具模拟 map

1
2
3
S1 = 'abc'
S2 = 'xyz123'
list(zip(S1, S2))
[('a', 'x'), ('b', 'y'), ('c', 'z')]
1
list(zip([-2, -1, 0, 1, 2]))
[(-2,), (-1,), (0,), (1,), (2,)]
1
list(zip([1, 2, 3], [2, 3, 4, 5]))   # zip 组合元素,在最短处截断
[(1, 2), (2, 3), (3, 4)]
1
list(map(abs, [-2, -1, 0, 1, 2]))
[2, 1, 0, 1, 2]
1
2
# 使用多个序列参数,map 以与 zip 配对元素相同的方式,把函数映射到取自每个序列的元素
list(map(pow, [1, 2, 3], [2, 3, 4, 5]))
[1, 8, 81]

编写自己的 map(func, …)

1
2
3
4
5
6
7
8
def mymap(func, *seqs):
res = []
for args in zip(*seqs):
res.append(func(*args))
return res

print(mymap(abs, [-2, -1, 0, 1, 2]))
print(mymap(pow, [1, 2, 3], [2, 3, 4, 5]))
[2, 1, 0, 1, 2]
[1, 8, 81]
1
2
3
# 使用列表解析
def mymap(func, *seqs):
return [func(*args) for args in zip(*seqs)]
1
2
3
4
5
6
7
8
# 使用生成器
def mymap(func, *seqs):
res = []
for args in zip(*seqs):
yield func(*args)

def mymap(func, *seqs):
return (func(*args) for args in zip(*seqs))

3. 解析语法总结

在 Python 3.X 中还有两种可用的解析表达式形式:集合解析和字典解析。

  • 对于集合,新的常量形式{1, 3, 2}等同于 set([1, 3, 2]),并且新的集合解析语法 {f(x) for x in S if P(x)} 就像是生成器表达式 set(f(x) for x in S if P(x)),其中 f(x) 是一个任意的表达式。
  • 对于字典,新的字典解析语法 {key: val for (key, val) in zip(keys, vals)}dict(zip(keys, vals)) 形式一样工作,并且 {x: f(x) for x in items} 像生成器表达式 dict((x, f(x)) for x in items) 一样工作。

Python 3.X 中所有解析替代方式的总结

1
2
# 列表解析:创建列表
[x * x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
1
2
# 生成器表达式:生成元素
(x * x for x in range(10))
<generator object <genexpr> at 0x0000019DAA566E08>
1
2
# 集合解析
{x * x for x in range(10)}
{0, 1, 4, 9, 16, 25, 36, 49, 64, 81}
1
2
# 字典解析
{x: x * x for x in range(10)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

3.1 作用域和解析变量

Python 3.X 中本地化四种形式的循环变量——生成器、集合、字典和列表解析中的临时循环变量名对于表达式是本地的。它们不和外部的名称冲突,也在外部不可用。和 for 循环迭代语句工作方式不同。

1
(M for M in range(5))
<generator object <genexpr> at 0x0000019DAA566F68>
1
M
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-23-370751da74d8> in <module>()
----> 1 M

NameError: name 'M' is not defined
1
2
X = 99
[X for X in range(5)]
[0, 1, 2, 3, 4]
1
X
99
1
2
Y = 99
for Y in range(5): pass # 循环语句不本地化变量名
1
Y
4

3.2 针对集合和字典的扩展的解析语法

集合和字典解析都支持嵌套相关的 if 子句从结果中过滤掉元素。

1
[x * x for x in range(10) if x % 2 == 0]
[0, 4, 16, 36, 64]
1
{x * x for x in range(10) if x % 2 == 0}
{0, 4, 16, 36, 64}
1
{x: x * x for x in range(10) if x % 2 == 0}
{0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

嵌套的 for 循环也有效。

1
2
[x + y for x in [1, 2, 3] for y in [4, 5, 6]]

[5, 6, 7, 6, 7, 8, 7, 8, 9]
1
{x + y for x in [1, 2, 3] for y in [4, 5, 6]}
{5, 6, 7, 8, 9}
1
{x: y for x in [1, 2, 3] for y in [4, 5, 6]}
{1: 6, 2: 6, 3: 6}

集合解析和字典解析也可以在任何类型的可迭代对象上迭代——列表、字符串、文件、范围以及支持迭代协议的任何其他类型。

1
{x + y for x in 'ab' for y in 'cd'}
{'ac', 'ad', 'bc', 'bd'}
1
{x + y: (ord(x), ord(y)) for x in 'ab' for y in 'cd'}
{'ac': (97, 99), 'ad': (97, 100), 'bc': (98, 99), 'bd': (98, 100)}
1
{k * 2 for k in ['spam', 'ham', 'sausage'] if k[0] == 's'}
{'sausagesausage', 'spamspam'}
1
{k.upper(): k * 2 for k in ['spam', 'ham', 'sausage'] if k[0] == 's'}
{'SPAM': 'spamspam', 'SAUSAGE': 'sausagesausage'}