我可以在Python列表推导式中给表达式起别名以避免多次求值吗?

21 投票
5 回答
6781 浏览
提问于 2025-04-16 10:52

我发现自己经常想写这样的Python列表推导式:

nearbyPoints = [(n, delta(n,x)) for n in allPoints if delta(n,x)<=radius]

这大概能说明我为什么想这么做,但有时候每个元素需要计算或比较多个值:

newlist = [(x,f(x),g(f(x))) for x in bigList if f(x)<p and g(f(x))<q]

所以我有两个问题:

  1. 那些函数会被多次计算吗,还是结果会被缓存起来?这个是语言规定的,还是说跟具体的实现有关?我现在用的是2.6版本,3.x会有不同吗?
  2. 有没有更简洁的写法?有时候f和g是很长的表达式,重复写容易出错,看起来也很乱。我真的希望能这样写:
newList = [(x,a=f(x),b=g(a)) for x in bigList if a<p and b<q]

但这样写不行。有没有什么好的理由不支持这种语法?能不能通过类似这个的方法实现?还是说我只能用多个列表推导式或者for循环?

5 个回答

4

当列表推导式变得越来越复杂时,它们也会变得很难阅读。在这种情况下,通常把它们的内部逻辑变成生成器函数,并给它们起一个(希望能)有意义的名字,会更好。

# First example
def getNearbyPoints(x, radius, points):
    """Yields points where 'delta(x, point) <= radius'"""
    for p in points:
        distance = delta(p, x)
        if distance <= radius:
            yield p, distance

nearbyPoints = list(getNearbyPoints(x, radius, allPoints))


# Second example
def xfg(data, p, q):
    """Yield 3-tuples of x, f(x), g(f(x))"""
    for x in data:
        f = f(x)
        if f < p:
            g = g(f)
            if g < q:
                yield x, f, g

newList = list(xfg(bigList, p, q))
13

更新:海象运算符 := 是在 Python 3.8 中引入的,它不仅可以给一个变量赋值,还能返回这个赋的值。根据 @MartijnVanAttekum 的回答,我建议在项目中使用它之前先等一年,因为 Python 3.6 和 3.7 仍然很流行。不过,这个运算符比我下面提到的别名建议要好用一些。

我有一个小技巧,可以在列表或字典推导式中创建别名。你可以使用 for alias_name in [alias_value] 这个方法。例如,你有一个比较耗时的函数:

def expensive_function(x):
    print("called the very expensive function, that will be $2")
    return x*x + x

还有一些数据:

data = [4, 7, 3, 7, 2, 3, 4, 7, 3, 1, 1 ,1]

然后你想对每个元素应用这个耗时的函数,并根据结果进行过滤。你可以这样做:

result = [
    (x, expensive)
    for x in data
    for expensive in [expensive_function(x)] #alias
    if expensive > 3
]

print(result)

第二个循环只会遍历一个大小为 1 的列表,实际上就相当于创建了一个别名。输出会显示这个耗时的函数被调用了 12 次,正好是每个数据元素调用一次。不过,这个函数的结果最多只会被使用两次,一次用于过滤,一次可能用于输出。

请务必像我这样将这些推导式分成多行,并在使用别名的那一行后面加上 #alias。如果你使用了别名,推导式会变得相当复杂,你应该帮助未来阅读你代码的人理解你在做什么。这可不是 Perl,你懂的 ;)

为了完整性,输出结果:

called the very expensive function, that will be $2
called the very expensive function, that will be $2
called the very expensive function, that will be $2
called the very expensive function, that will be $2
called the very expensive function, that will be $2
called the very expensive function, that will be $2
called the very expensive function, that will be $2
called the very expensive function, that will be $2
called the very expensive function, that will be $2
called the very expensive function, that will be $2
called the very expensive function, that will be $2
called the very expensive function, that will be $2
[(4, 20), (7, 56), (3, 12), (7, 56), (2, 6), (3, 12), (4, 20), (7, 56), (3, 12)]

代码:http://ideone.com/7mUQUt

11

关于第一个问题,是的,它们会被多次计算。

关于第二个问题,做法是把计算和筛选分开来写:

简化版:

[(x,fx,gx) for (x,fx,gx) in ((x,fx,g(fx)) for (x,fx) in ((x,f(x)) for x in bigList) if fx < p) if gx<q]

扩展的长版,方便理解:

[(x,f,g) for (x,f,g) in
  ((x,f,g(f)) for (x,f) in
     ((x,f(x)) for x in bigList)
  if f < p)
if g<q]

这样做会尽量减少对 fg 的调用次数(如果 f(x) 的值不小于 p,那么 g 就不会被调用,而 f 对于 bigList 中的每个值只会被调用一次)。

如果你想要代码更整洁,可以使用中间变量:

a = ( (x,f(x)) for x in bigList )
b = ( (x,fx,g(fx)) for (x,fx) in a if fx<p )
results = [ c for c in b if c[2] < q ] # faster than writing out full tuples

ab 使用生成器表达式,这样它们就不需要真正创建列表,而是在需要的时候才进行计算。

撰写回答