LOADING

加载过慢请开启缓存 浏览器默认开启

python-推导式

python-推导式

在我们写代码时,经常会遇到将一个列表(可迭代对象)中的每一个元素进行统一的操作,后再保存回列表的场景。

这时就可能写出以下类似的代码:

lst = [1, 2, 3, 4, 5]
result = []
for n in lst:
    n += 1  # 对list中的元素进行操作
    result.append(n) # 存入result

python中提供了一种简便的推导式(comprehensions)语法,可以简化以上类似的操作

推导式的基本用法

result = [item for item in input_lst]
# input_lst 输入的列表(可迭代对象)
# item 在循环体中使用的变量,即输入的列表(可迭代对象)中的元素;第一个item的值将作为result中的元素返回

例如:

input_lst = [1, 2, 3, 4, 5] 

result = [item*10 for item in input_lst]
# result == [10, 20, 30, 40, 50]

推导式中的条件判断

在基本用法的基础上,可以添加对输入的列表(可迭代对象)进行条件判断;在条件成立时返回到结果中

input_lst = [1, 2, 3, 4, 5] 

result = [item for item in input_lst if item%2 != 0]
# if item% 2!= 0 为该例的条件,即取列表中的质数

可能推导式还会让你感到疑惑

但是将其写成其对应的for loop形式就一目了然了

input_lst = [1, 2, 3, 4, 5] 
result = []
for item in input_lst:
    if item%2 != 0:
        result.append(item)

推导式的变量

以上不难看出其和普通的for loop是没有多大区别的,所以我们可以在推导式中不使用循环体变量、判断是不已可迭代对象中元素为依据、不以元素处理结果作为返回结果;以下为极端例子:

result = [ 1 for _ in range(10) if True ]
# result == [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
# 以上我用 1 作为返回结果; 丢弃循环体变量;判断条件始终为True

推导式的嵌套

for loop一样;在推导式中也可以嵌套:

result = [ i*j for i in range(5) if i%2==0 for j in range(5) if j%2!=0]

# 等价的for loop
result = []
for i in  range(5):
    if i%2 == 0:
        for j in range(5):
            if j%2 != 0:
                result.append( i*j )

其它的推导式

当然除了最常见的list推导式;还有对应的字典、集合的推导式。

字典推导式

result = {k: v*2 for k,v in {"a":99, "b":77, "c":55}.items()}

集合推导式

result = {i*2 for i in range(10)}

生成器推导式

看了列表、字典、集合的推导式,是不是很自然的相对使用圆括号()可以生产元组的推导式;很遗憾这是不行的。

使用圆括号()的推导式产生的的一个生成器generator

result = (i for i in range(10))
print( type(result) ) # <class 'generator'>

生成器推导式在函数传参时的语法糖;在以实参传入函数时可省略圆括号()

# 有函数 fun 需要一个可迭代对象作为参数

fun( [i for i in range(10)] ) # 传入 list
fun( {i for i in range(10)} ) # 传入 set
fun( i for i in range(10) ) # 传入 generator

推导式的本质

推导式的本质就是定义了一个立即执行函数:

result = [ i*i for i in range(10) ]
# 等价的函数 
def __comp(lst):
    tmp = []
    for i in lst:
        tmp.append( i*i )
    return tmp
result = __comp( range(10) )
del __comp

生成器推导式:

result = ( i*i for i in range(10) )

# 生成器推导式的等价的函数 
def __gen(exp):
    for i in exp:
        yield i*i
        
g = __gen( iter(range(10)) )  
del __gen

在推导式中只有第一层的for循环是类似函数传参将外层变量传入推导式中;其它的变量只会在执行时刻再去寻值

也就是说在[i*j for i in lst for j in lst] 中的两个lst可能不是一个东西; 这一点在生成器推导式中尤其需要注意

注意以上只是为了解释;不代表python是这样实现的!!

推导式的坑

在类中使用时注意

且看以下代码,其会执行成功吗;结果是?

class Cls:
    a = 2
    lst = [1,2,3,4,5]
    lst_double = [ n*a for n in lst ]

以上代码会报错:

NameError: name 'a' is not defined

只是为什么呢?上文说到推导式是定义了一个函数;如果我们将推导式改写成对应的函数等价形式或许可以窥得一二:

class Cls:
    a = 2
    lst = [1,2,3,4,5] 
    def __comp(lst):
        tmp = []
        for i in lst:
            tmp.append( i*a ) # 无法获取到Cls中的a变量
        return tmp
    lst_double =  __comp(lst)

还记得class中的类变量怎么访问吗;Cls.aself.a 是的,在类中定义的函数是不能直接通过变量名访问到类变量的;也就是说在上例的推导式中的a变量只能是从全局中去获取的,而全局中又没有定义变量a,所以才报了这个错误。

以上代码可改成:

class Cls:
    a = 2
    lst = [1,2,3,4,5]
    lst_double = (lambda lst,a: [ n*a for n in lst ])(lst, a)

# -----------------------
class Cls:
    a = 2
    lst = [1,2,3,4,5] 
    def __lambda(lst, a):
        def __comp(lst):
            tmp = []
            for i in lst:
                tmp.append( i*a ) # 以闭包的形式拿到a的值
            return tmp
        return __comp(lst)
    
    lst_double =  __lambda(lst, a)

生成器推导式的坑

以下代码执行结果是?

lst = [ 1, 2, 3, 4, 5 ]
gen = ( i for i in lst if i in lst )
lst = [ 1, 2, 3 ]
print( list(gen) )

结果是:[ 1, 2, 3 ]

在生成器推导式中只有第一层的for循环是定义时被立即估值的;而其它的变量都会在执行该生成器时到对应的命名空间中寻找。

上面的代码等于以下代码:

lst_1 = [ 1, 2, 3, 4, 5 ]
lst_2 = [ 1, 2, 3 ]
gen = ( i for i in lst_1 if i in lst_2 )
print( list(gen) )

#--- 或是 ------------------------------
lst = [ 1, 2, 3, 4, 5 ]
def __gen(exp):
    for i in exp:
        if i in lst:
            yield i
            
lst = [1, 2, 3]
g = __gen( iter(lst) )  
print( list(g) )
lst = [1, 2, 3, 4, 5]
g = (i for i in lst if lst)  # 此时 for i in lst 部分的lst 已经固定下来了; lst == [1, 2, 3, 4, 5]
print(next(g))
print(next(g))
lst = False  
print(next(g))  # 因为条件为False, 所以在生成器中yield不出东西来; 故报错 StopIteration

结束语

合理的使用python提供的推导式,可以简化代码,而且推导式的性能可能会比普通的for循环好;使写出来的代码更加的优雅。当然应该尽量避免在推导式中进行多重的循环判断;这可能会破坏代码的可读性。在看一个推导式时需要仔细推算其结果时不如考虑考虑将其改写成普通的for循环形式。

链接

https://www.bilibili.com/video/BV1PG411x765

https://www.bilibili.com/video/BV1pY411n722

https://peps.python.org/pep-0202/