我能否在Python列表推导式中使用别名来避免多次评估表达式?

21

我经常想按照以下方式编写 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个回答

13
更新: Python 3.8中引入了海象操作符:=,它既赋值给变量,同时也将被赋的值作为结果返回。根据@MartijnVanAttekum的回答,我建议在项目中使用它之前等待一年左右,因为Python 3.6和3.7仍然很流行,但它比我下面提出的别名建议更好。

我有一个hack可以在列表/字典推导式中创建别名。你可以使用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)

第二个for循环只会遍历长度为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

关于问题#1,是的,它们将被多次评估。

关于问题#2,处理方法是在不同的推导式中进行计算和过滤:

简化版:

[(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,并且对于bigList中的每个值,f只会被调用一次)。 如果您喜欢,您也可以通过使用中间变量来获得更整洁的代码:
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使用生成器表达式,这样它们就不必实际实例化列表,并且只在需要时被评估。


+1 虽然我对元组索引有所疑虑。此外,在某些情况下,函数或生成器函数是最佳选择。 - Apalala
这段代码看起来比@ncoghlan或我自己的解决方案难以阅读。 - Herbert
@Amber,这段代码是否有冗余之处?for (x, f(x)) in ((x, f(x)) for x in bigList) if f(x) < p - Ramin Melikov

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))

4

2021更新

  1. 在Python 3.8中引入了海象操作符(分配表达式),现在可以使用别名。例如,使用difference作为对delta()所计算结果的别名:

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

参考: PEP 572


3
  1. 如果在表达式中(包括列表推导式)两次调用一个函数,它将被真正地调用两次。Python无法知道你的函数是纯函数还是过程型函数。它会在你告诉它的情况下调用它,即在这种情况下调用两次。

  2. 在列表推导式中无法给变量赋值,因为在Python中,赋值是语句而不是表达式。

听起来你应该使用完整的循环而不是列表推导式。


1
我不想在推导式内部编写语句,只是希望有一些语法糖来避免再次输入。也许像“a:=f(x)”这样的东西会更好。但正如你指出的那样,由于函数被评估了第二次,这并没有什么帮助。 - krashalot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,