Last Updated: 2024-05-08 07:56:45 Wednesday
-- TOC --
写出正确高速且优雅的Python代码,我想是每个喜欢Python语言的程序员的愿望和癖好。使用List Comprehension技巧,可以同时达成这3个目标。
英文简写:
listcomp
List Comprehension这个词不太好翻译,有人翻译成列表生成器
,这是从功能角度的翻译,它的确是生成了一个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的速度很快,比使用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]
复杂if条件:
>>> [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中的表达式,一层层展开而已。
下面是一个性能测试:
>>> 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的组合明显更快!
也许是因为list使用太普遍太handy了,我在很长一段时间内,只知道listcomp,也知道可以用相似的语法创建dict和set,但是却不知道,其实在用comprehension语法创建dict或set的时候,它们可以被叫做dictcomp
和setcomp
。
>>> {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'} # dictcomp
{'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}
英文简写:
genexpr
,就是用expression创建generator。
当使用()
的时候,不是创建tuple,而是另一个概念了,叫做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来优化内存!
变量具有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 gone
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'd' is not defined.
listcomp炫技装逼:
>>> 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]
还有一种更简单地写法:
print([y for x in values if (y:=func(x))])
注意:如果func可能raise,需要妥善处理。
iterator就是那种只能遍历一次的对象。而iterable是可以多次遍历的对象,只要对象还存在。
generator就是一种典型的iterator。
下面是使用any和all接口的示例,同时展示walrus operator在这里的妙用:
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 --