在循环中测试一个不会改变的条件

6
有时我需要检查循环内不会改变的条件,这意味着测试在每次迭代中都会被评估,但我认为这并不是正确的方法。
我认为既然条件在循环内不会改变,那么我应该在循环外仅测试一次该条件,但这样做将不得不"重复自己",可能需要写多个相同的循环。下面是演示我的代码:
#!/usr/bin/python

x = True      #this won't be modified  inside the loop
n = 10000000

def inside():
    for a in xrange(n):
        if x:    #test is evaluated n times
            pass
        else:
            pass
    
def outside():
    if x:        #test is evaluated only once
        for a in xrange(n):  
            pass
    else:
        for a in xrange(n):
            pass

if __name__ == '__main__':
    outside()
    inside()

对之前的代码运行 cProfile,输出结果如下:

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.542    0.542    0.542    0.542 testloop.py:5(inside)
        1    0.261    0.261    0.261    0.261 testloop.py:12(outside)
        1    0.000    0.000    0.803    0.803 testloop.py:3(<module>)

这表明,显然,在循环外进行一次测试可以获得更好的性能,但我不得不写两个相同的循环(如果有一些elif,则可能需要写更多)。
我知道在大多数情况下这种性能并不重要,但我需要知道编写此类代码的最佳方法。例如,是否有一种方法可以告诉Python仅评估一次测试?
感谢任何帮助。
编辑:
实际上,经过一些测试后,我现在相信性能差异主要受到循环内执行的其他代码的影响,而不是测试的评估。因此,目前我将坚持第一种形式,这种形式更易读,并且以后更易于调试。

如果循环内部有某个条件,你知道下一次迭代不会改变,那么就可以使用 break 来跳出循环。 - Samy Vilar
@samy.vilar 当然,但我只是举了一个最小的例子,但在大多数情况下,会有比那些“pass”的更多代码 :) - Amr
如果您需要在循环外检查条件,请这样做,更新/设置变量,然后在循环内使用。相应地,重复/冗余的代码是不好的,尤其是在循环中。创建另一个变量可能会或可能不会解决这个问题,这取决于您想要实现什么。 - Samy Vilar
7个回答

5
首先,您示例之间性能差异的一个主要组成部分是查找全局变量所需的时间。如果我们将其捕获到本地变量中:
def inside_local():
    local_x = x
    for a in xrange(n):
        if local_x:
            pass
        else:
            pass

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    1    0.258    0.258    0.258    0.258 testloop.py:13(outside)
    1    0.314    0.314    0.314    0.314 testloop.py:21(inside_local)
    1    0.421    0.421    0.421    0.421 testloop.py:6(inside)

大多数性能差异都会消失。
通常情况下,只要有共同的代码,您应该尝试将其封装起来。如果if的分支除了循环之外没有任何共同点,那么尝试将循环迭代器(例如生成器)封装起来。

我选择了这个答案,因为它提供的信息比其他答案更多。 - Amr

5

这是我通常在这种情况下所做的事情。

def inside():
    def x_true(a):
        pass

    def x_false(a):
        pass

    if x:
        fn = x_true
    else:
        fn = x_false

    for a in xrange(n):
        fn(a)

谢谢,将变量设置为函数总是跑到我的脑海里 xD - Amr
注意:这可能会很慢,因为(a)x_truex_false是闭包,而(b)函数调用会引入开销。但这绝对是最干净的选项。 - ecatmur
@ecatmur:我考虑了函数调用开销,但是闭包比普通函数慢吗? - Amr
@Amr 稍微有点,是的;在分析中,当闭包被移动到模块级别时,我得到了2.803的值,而在原始的 outsideinside 中,分别为0.292和0.390,因此它受到函数调用开销的影响。 - ecatmur
正如ecatmur所说,我已经测试过了,结果证明速度要慢得多。 - Amr

3

Python有闭包、lambda函数,将函数视为一等公民和许多内置函数,这些都帮助我们消除重复的代码。例如,想象一下您需要将一个函数应用于一系列值,您可以按照以下方式完成:

def outside():              
    if x:        # x is a flag or it could the function itself, or ...
        fun = sum # calc the sum, using pythons, sum function
    else:
        fun = lambda values: sum(values)/float(len(values)) # calc avg using our own function

    result = fun(xrange(101))

如果您提供一个准确的场景,我们可以帮助您进行优化。

2
我不知道有哪种解释性语言支持这个方向的支持,编译语言可能只会进行一次比较(循环不变式优化),但如果x的评估是简单的,则帮助不大。 显然,用作“pass”语句替代的代码不能完全相同,因为这样就没有使用“if”的必要了。通常情况下,应编写一个在两个位置都调用的过程。

1
def outside():
    def true_fn(a):
        pass
    def false_fn(a):
        pass

    fn = true_fn if x else false_fn
    for a in xrange(n):
        fn(a)

我认为您不应该在if语句的末尾加上冒号“:”。另外,true_fn和false_fn的值是什么?运行时会出现错误。回溯(最近调用最多的)如下所示: File "<stdin>", line 1, in <module> File "test.py", line 7, in outside fn = true_fn if x else false_fn NameError: global name 'true_fn' is not defined. - octopusgrabbus

0
在您的情况下,这取决于您想要什么:可读性还是性能。
如果您正在执行某种过滤任务,您也可以使用list_comprehension来运行循环:
[e for e in xrange(n) if x]

如果您展示一下更多的代码,我可以给出一些建议。

0

根据您最初的问题,您想在不消耗大量系统资源的情况下测试x的值,您已经接受了涉及将全局x的值复制到本地变量的答案。

现在,如果返回x的值涉及多步函数,但您保证x的结果始终相同,则我会考虑对函数进行记忆化处理。 这里有一个非常好的stackoverflow link 关于这个主题


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