我可以在Python列表推导式中给表达式起别名以避免多次求值吗?
我发现自己经常想写这样的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]
所以我有两个问题:
- 那些函数会被多次计算吗,还是结果会被缓存起来?这个是语言规定的,还是说跟具体的实现有关?我现在用的是2.6版本,3.x会有不同吗?
- 有没有更简洁的写法?有时候f和g是很长的表达式,重复写容易出错,看起来也很乱。我真的希望能这样写:
newList = [(x,a=f(x),b=g(a)) for x in bigList if a<p and b<q]
但这样写不行。有没有什么好的理由不支持这种语法?能不能通过类似这个的方法实现?还是说我只能用多个列表推导式或者for循环?
5 个回答
当列表推导式变得越来越复杂时,它们也会变得很难阅读。在这种情况下,通常把它们的内部逻辑变成生成器函数,并给它们起一个(希望能)有意义的名字,会更好。
# 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))
更新:海象运算符 :=
是在 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)]
关于第一个问题,是的,它们会被多次计算。
关于第二个问题,做法是把计算和筛选分开来写:
简化版:
[(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]
这样做会尽量减少对 f
和 g
的调用次数(如果 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
a
和 b
使用生成器表达式,这样它们就不需要真正创建列表,而是在需要的时候才进行计算。