理解List Comprehension语法

Last Updated: 2023-05-21 08:25:11 Sunday

-- TOC --

写出正确快速且优雅的Python代码,我想是每个喜欢Python语言的程序员的愿望和癖好。使用List Comprehension技巧,可以同时达成这3个目标。

List Comprehension

英文简写:listcomp

List Comprehension(后面简写为listcomp)这个词不太好翻译,有人翻译成列表生成器,这是从功能角度的翻译,它的确是生成了一个list。listcomp通过简练的代码(通常只有一行),实现了复杂的for loop和if判断,最后生成一个list对象。

下面开始以具体例子展开介绍:

>>> [i for i in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>
>>> a = []
>>> for i in range(10):
...   a.append(i)
... 
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

以上代码用两种方式来创建list,使用for loop的可读性比listcomp要差一些,因为通过for loop可以做很多其它事情,而listcomp的目的单一,就是生成一个list。而且,listcomp的速度很快,listcomp的速度比使用filter和map的组合还要快!(后文有速度测试)

在listcomp中,可以有if判断,还可以多个for嵌套:

>>> [i for i in range(10) if i&0x01]
[1, 3, 5, 7, 9]
>>> [i+j for i in range(10) for j in range(2)]
[0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10]
>>> [i+j for i in range(10) for j in range(2) if i&0x01]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> [i+j for i in range(10) for j in range(2) if i&0x01 if j&0x01]
[2, 4, 6, 8, 10]

从loop中得到一个个的i,然后将一个个i排列起来,生成list,这个过程中,可以增加if条件判断,甚至嵌套的for循环。

下面展示一下复杂条件的写法:

>>> [i for i in range(10) if i>3 if i<9 if i%2==0]
[4, 6, 8]

多个条件是and关系,其实写在一个if里面也可以:

>>> [i for i in range(10) if i>3 and i<9 and i%2==0]
[4, 6, 8]

可以对筛选出来的变量,进行更多的计算,比如调用某个函数接口:

>>> [str(i) for i in range(10)]
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

注意:如果调用的函数接口有可能抛出异常,一般需要在此接口内部妥善处理异常。

Nested List Comprehension,嵌套的listcomp,就是表达式中有多个loop,多重循环:

>>> [(i,j) for i in range(3) for j in range(3)]
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
>>> a = []
>>> for i in range(3):
...   for j in range(3):
...     a.append((i,j))
...
>>> a
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]

再带上一个条件:

>>> a = [(i,j) for i in range(3) for j in range(3) if i<j]
>>> a
[(0, 1), (0, 2), (1, 2)]

综上,我们来写一个超级复杂的listcomp:

>>> [(i,j) for i in range(5) for j in range(5) if i<j if i%2==0 and j%2==0]
[(0, 2), (0, 4), (2, 4)]
>>> a = []
>>> for i in range(5):
...   for j in range(5):
...     if i<j:
...       if i%2==0 and j%2==0:
...         a.append((i,j))
...
>>> a
[(0, 2), (0, 4), (2, 4)]

看出来门道了吗?其实就是将listcomp中的表达式,一层层展开而已。

listcomp性能测试

下面是一个性能测试:

>>> stmt01
'a = [str(i) for i in range(100) if i&0x01]\n'
>>> stmt02
'a = list(map(str,filter(lambda x:x&0x01,range(100))))\n'
>>>
>>> from timeit import repeat
>>> import time
>>> repeat(stmt01, timer=time.process_time, number=10000)
[0.14624905000000155, 0.1329856499999984, 0.13321112299999882, 0.1342888209999984, 0.13319650800000105]
>>> repeat(stmt02, timer=time.process_time, number=10000)
[0.2267971529999997, 0.17540836399999904, 0.1756132500000014, 0.17685208700000032, 0.1756925690000024]

生成相同的list对象,listcomp比filter和map的组合更快!

dictcomp & setcomp

也许是因为list使用太普遍太handy了,我在很长一段时间内,只知道listcomp,也知道可以用相似的语法创建dict和set,但是却不知道,其实在用comprehension语法创建dict或set的时候,它们可以叫做dictcompsetcomp

>>> {i:0 for i in range(10)}  # dictcomp
{0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0}
>>> {i for i in range(10) if i&0x01}  # setcomp
{1, 3, 5, 7, 9}
>>>
>>> a = ['a','b','c','d']
>>> {v:i for i,v in enumerate(a) if i%2==0 if v!='b'}
{'a': 0, 'c': 2}

使用setcomp,依然可以实现去重的功能:

>>> {i for i in (1,1,2,2,3,3,4,4,)}
{1, 2, 3, 4}

使用dictcomp,出现重复的key时,覆盖:

>>> {a:b for a,b in [(1,1),(1,2),(1,3)]}
{1: 3}

Generator Expressions

英文简写:genexpr

当使用()的时候,就是另一个概念,叫做generator expression:

>>> d = (i for i in range(10))
>>> d
<generator object <genexpr> at 0x0000013B549CDDD0>
>>> for i in d:
...   print(i)
...
0
1
2
3
4
5
6
7
8
9

()中使用与listcomp一样的语法,得到的不是一个tuple,而是一个generator。创建tuple,只需要简单地变通一下即可:

>>> a = tuple(i for i in range(10))
>>> a
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

Generator Expression不会生成list对象(或dict或set),而是个generator。我们都知道generator的一个优势,就是内存。遍历一个list,与遍历一个generator,前者需要提前准备好完整的list,而后者就不需要,想象一下如果list的长度上千万呢!

如果不需要创建list,dict,tuple或set对象,可以考虑使用generator来优化内存!

Listcomp或Genexpr中的变量

local scope

>>> a = '123'
>>> b = [a for a in a]
>>> b
['1', '2', '3']
>>> a
'123'

Walrus operator是个例外:

>>> a = '123'
>>> b = [c:=d for d in a]
>>> c  # c is accessible
'3'
>>> d  # d is just gone
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'd' is not defined.

一行代码生成九九乘法表

>>> print('\n'.join([' '.join(["%sx%s=%-2s"%(j,i,i*j) for j in range(1,i+1)]) for i in range(1,10)]))
1x1=1
1x2=2  2x2=4
1x3=3  2x3=6  3x3=9
1x4=4  2x4=8  3x4=12 4x4=16
1x5=5  2x5=10 3x5=15 4x5=20 5x5=25
1x6=6  2x6=12 3x6=18 4x6=24 5x6=30 6x6=36
1x7=7  2x7=14 3x7=21 4x7=28 5x7=35 6x7=42 7x7=49
1x8=8  2x8=16 3x8=24 4x8=32 5x8=40 6x8=48 7x8=56 8x8=64
1x9=9  2x9=18 3x9=27 4x9=36 5x9=45 6x9=54 7x9=63 8x9=72 9x9=81

多重条件写在一行

下面示例的条件,与之前基础讲解的条件不同,这里是已经将某个值成功提取出来,在具体使用的时候,进行多重条件判断。

values = [True, False, True, None, True]
print(['yes' if v is True else 'no' if v is False else 'unknown' for v in values])
# ['yes', 'no', 'yes', 'unknown', 'yes']

# Above is equivalent to:
result = []
for v in values:
    if v is True:
        result.append('yes')
    else:
        if v is False:
            result.append('no')
        else:
            result.append('unknown')

print(result)
# ['yes', 'no', 'yes', 'unknown', 'yes']

上例的核心,其实不是List Comprehension,而是条件判断,anyway......show一下这种if写法:

>>> a = 100
>>> 'low' if a<3 else 'middle' if a<6 else 'high' if a<10 else 'boom'
'boom'

避免额外的函数调用

def func(val):
    return val > 4  # Expensive computation...

values = [1, 4, 3, 5, 12, 9, 0]
print([func(x) for x in values if func(x)])  # Inefficient
# [True, True, True]
print([y for y in (func(x) for x in values) if y])  # Efficient
# [True, True, True]

最后一行代码,其实用到了前面介绍的一个知识点,使用()创建的时候generator,这个List Comprehension,其实遍历的是generator。

还有一种简单地写法:

print([y for x in values if (y:=func(x))])

注意:如果func可能raise,需要妥善处理。

创建iterator

iterator就是那种只能遍历一次的对象。而iterable是可以多次遍历的对象,只要对象还存在。

generator就是一种典型的iterator,下面是使用any和all调用的示例,这两个内置的接口,入参为iterable:

numbers = [1, 4, 6, 2, 12, 4, 15]

# Only returns boolean, not the values
print(any(number>10 for number in numbers))  # True
print(all(number<10 for number in numbers))  # False

# the first number which bigger than 10
any((value:=number)>10 for number in numbers)  # True
print(value)  # 12

# the first number which could not satisfy <10
all((counter_example:=number)<10 for number in numbers)  # False
print(counter_example)  # 12

未完待续...

本文链接:https://cs.pynote.net/sf/python/202209181/

-- EOF --

-- MORE --