在Python中有没有更快的while循环方法?

1 投票
3 回答
7214 浏览
提问于 2025-04-18 01:26

我刚花了一个小时在我的代码里找问题,因为我重写了很多代码后,发现它变得很慢。没想到原来是因为while循环的速度这么慢。我想我之前从来没有在时间要求很严格的地方用过它。

最后我把问题缩小到这个方法上,经过测试发现,while循环里面的两行代码运行得非常快,大约是每100000次中有30次到每10000次中有1次的时间。但是当我把我的时间调用放在while循环外面时,速度就慢到大约1秒。

def query(self, command):
    result = ''
    result_list = []
    self.obd.write(command + "\r\n")
    while result != '>':
        result = self.obd.readline()
        result_list.append(result)
    self.result = result_list[-2].strip()

为什么while循环会这么慢,我该怎么加快速度呢?

为了说明我在做什么,我是从一个设备获取串行输入,这个设备输出的行数似乎总是变化。有时候我需要的信息在第二行,有时候在第三行,有时候在第一行。我唯一确定的是,它是在“>”符号之前的最后一行,而我尝试过的其他方法会让我得到一些无法读取的缓存数据,导致后续出错,所以我必须等到“>”出现。

编辑:显然我没有解释清楚我做了什么。

我从上面的代码开始,编辑它以检查运行速度。

def query(self, command):
    result = ''
    result_list = []
    self.obd.write(command + "\r\n")
    while result != '>':
        a = datetime.datetime.now()
        result = self.obd.readline()
        result_list.append(result)
        b = datetime.datetime.now()
        print b - a
    self.result = result_list[-2].strip()

每次运行这个方法平均不到1/10000秒。

def query(self, command):
    result = ''
    result_list = []
    self.obd.write(command + "\r\n")
    a = datetime.datetime.now()
    while result != '>':
        result = self.obd.readline()
        result_list.append(result)
    b = datetime.datetime.now()
    print b - a
    self.result = result_list[-2].strip()

而这个方法每次运行都要1秒以上。

在while循环里面发生的事情是正在读取串口。如果我在它周围加一个for循环,它能运行一段时间,但当缓存稍微滞后时就会停止,不过我可以以每秒60次的频率查询端口。

如果问题不在while循环,那我为什么会得到这样的结果呢?

3 个回答

0

我最开始也不太确定问题出在哪里,这让我有点烦,但没烦到我去拆别人的代码。最后我还是找到了一种有效的解决方案。

我决定不再使用readline(),而是用read(1),这个方法每次从缓冲区读取一个字节。这样做后,我就能等待“>”这个字符,并返回前一行。

这是我新的方法:

def query(self, command):
    line = ''
    self.obd.write(command + "\r\n")
    while True:
        c = self.obd.read(1)
        line += c
        if c == '>':
            break
    # should work even if there is no newline character
    self.result = line.split('\r')[-2].strip()

这个方法的运行速度和我之前用for循环的方法差不多,都是大约60次每秒,但它更不容易让缓冲区里堆满无用的数据。

感谢大家的帮助,让我找到了正确的方向。

2

其实,while循环并不慢。实际上,while循环的开销几乎是感觉不到的。如果你觉得循环外的语句执行得很快,那可能是你的测量有问题。

从文件或串口设备读取数据是最慢的操作之一。因为你的while循环里有一个读取行的语句,这可能就是导致它变慢的原因。也许它在等待输入的一行数据,而这种等待就是让它变慢的原因。

你提到把日期时间的调用从循环里移到外面,但我在这里没有看到任何日期时间的函数,所以很难猜测这是否是问题的一部分。

3

为了更清楚地说明,在所有解释型语言(比如Python)中,循环的速度比在编译型语言(比如C)中要慢得多。这不仅适用于while循环,也适用于for循环等其他类型的循环。原因在于,解释型语言在每执行一条语句时都有较高的开销,而大多数编译型语言则几乎没有这种开销。这种开销在每次循环迭代时都会产生,即使使用列表推导式也是如此。对于解释型语言,有至少三种方法可以优化或减轻循环的影响:

  • 优化每次循环的迭代,以实现更快的运行时间。
  • 使用内置的操作,这些操作针对特定任务进行了良好的优化。
  • 使用像numpy这样的库,它们提供了“向量化”的函数。(这是处理数字数据时的最佳解决方案。)这些库通常部分使用编译代码来加速重复操作。

在你的情况下,我建议你可以尝试第一种方法(通过只存储一个标量而不是数组来优化内部):

def query(self, command):
    result = ''
    line = ''
    self.obd.write(command + "\r\n")
    while line != '>':
        result = line
        line = self.obd.readline()
    self.result = result.strip()

append函数比简单的标量赋值花费更多时间,因此可以节省一点时间,而且你的代码已经忽略了除了倒数第二行以外的所有行。

或者,你可以尝试使用一个经过良好优化的内置函数。如果obd支持readline(),那么它很可能是一个类似文件的对象,也会支持readlines()read()。根据数据的长度和复杂性,使用re.searchread()的结果有时会更快:

def query(self, command):
    self.obd.write(command + "\r\n")
    result = re.search('(?:^|\n)([^\n]*?)\n>(\n|$)', obd.read())
    self.result = result.group(1) if result else None

这里的正则表达式看起来并没有那么复杂。它只是搜索一行后面跟着一行等于>的内容。效率也不是特别高。

最后一种方法是使用非正则的内置函数,减少你的while循环运行的次数:

def query(self, command):
    self.obd.write(command + "\r\n")
    remaining = obd.read().lstrip()
    sep = ''
    while remaining and remaining[0] != '\n':
        chunk, sep, remaining = remaining.partition('\n>')
    self.result = chunk.rpartition('\n')[2] if sep else ''

这样只会在每行开头出现>时运行一次while循环,而这可能在整篇文件中只出现一次。

需要注意的是,后面提到的两个改动(正则表达式和使用partition)都依赖于先将整个文件读取到内存中。这里有两个副作用需要注意:

  • 读取整个文件所需的内存与将整个文件添加到列表中所需的内存一样多,因此与之前的方法相比,没有节省内存,只有时间上的节省。
  • 因为整个文件都被读取,所以>后面的字节/行也会被读取,如果obd没有发送EOF信号(比如如果它是一个不会关闭的管道),那么会导致失败。特别是如果你打算让另一个文件继续从obd读取时,要特别注意这一点。

撰写回答