【问题标题】:Python: Speed of loop drastically increases if different run order?Python:如果不同的运行顺序,循环速度会急剧增加?
【发布时间】:2021-08-29 09:28:23
【问题描述】:

在编写脚本以纠正 OCR 生成的文档中的格式错误时,我遇到了一个问题,根据我首先运行的循环,程序运行速度会慢 80%。

这是代码的简化版本。我有以下循环来检查大写错误(例如“posSible”):

def fixUppercase(doc):
    fixedText = ''
    for line in doc.split('\n'):
        fixedLine = ''
        for word in line.split():
            if (
                word.isalpha()
                and (
                     word.isupper()
                     or word.istitle()
                     or word.islower()
                )
            ):
                if word == line.split()[-1]:
                    fixedLine += word + '\n'
                else:
                    fixedLine += word + ' ' 
            elif (
                word.isalpha()
            ):
                lower = word.lower()
                if word == line.split()[-1]:
                    fixedLine += lower + '\n'
                else:
                    fixedLine += lower + ' '
            else:
                if word == line.split()[-1]:
                    fixedLine += word + '\n'
                else:
                    fixedLine += word + ' ' 
        fixedText += fixedLine  
    
    return fixedText

以下循环检查并删除标题:

def headingsFix(doc):
    fixedText = ''
    count = 0
    stopWords = ['on', 'and', 'of', 'as', 'for']
    for line in doc.split('\n'):
        tokenLine = ''
        for word in line.split():
            if word not in stopWords:
                tokenLine += word + " "
        if tokenLine.istitle() and (
            not line.endswith('.') 
            and not line.endswith(',')
            and not line.endswith(')')
            and not line.endswith(';')
            and not line.endswith(':')
        ):

            count += 1
        else:
            fixedText += line
            
    return fixedText

这是fixedUppercase 函数中的循环大大减慢了速度。如果我在该函数之前运行任何其他函数或循环,或者如果我先运行该函数或完全删除它,则程序很快。如果两个循环都是一个函数的一部分,则行为相同。

我认为可能是另一个函数或循环通过扩展文档的长度导致了错误,但是使用len() 进行检查显示了相同的文档大小。

【问题讨论】:

  • 您能否更明确地说明“...如果我在此之前运行任何其他功能或循环,速度会大大降低”?您能否举一个具体的例子,首先运行另一个函数,使那个函数明显变慢?
  • 您会在此处找到一些有用的指南来创建最小的可重现示例:stackoverflow.com/help/minimal-reproducible-example 让您了解任何试图回答您的问题的人所面临的问题:1. 我们不知道您的输入 - 它有长线吗?多行?很多话? 2. 我们不知道如何运行你的代码来重现你的问题——我们应该先运行什么其他函数,我们应该怎么做,哪些输入和输出去哪里?
  • 嗨,我确实举了一个例子:如果我在 fixUppercase() 之前运行 headingsFix(),后者需要大约 80% 的时间才能完成。附言我知道我可以用正则表达式完成同样的工作,我只是好奇为什么速度差异。
  • 嗨 Wee​​ble,输入是字符串。您可以尝试使用任何字符串。观察。字符串越小,时间和差异就越小。 300k字,就是几分钟。只需将一个字符串传递给一个函数,然后传递另一个函数,计时,然后以相反的顺序再次执行。
  • 不运行它,这可能很慢,因为它会构建大量字符串,这在 Python 中非常慢,因为字符串是不可变的,因此每次都必须创建一个新的!你可以使用timeit比较Python中函数的速度!

标签: python python-3.x function


【解决方案1】:

headingsFix 删除所有行尾,这可能不是您想要的。但是,您的问题是关于为什么更改转换顺序会导致执行速度变慢,所以我不会在这里讨论解决这个问题。

fixUppercase 在处理多字行时效率极低。它在整个书本长度的字符串上一遍又一遍地调用line.split()。如果每行可能有十几个单词,这并不是很慢,但是如果你有一个包含数万个单词的巨大行,它就会变得非常慢。我发现你的程序运行得更快,这个改变只分割了每行一次。 (我注意到我不能说您的程序是否正确,只是这种更改应该具有相同的行为,同时速度要快得多。恐怕我不是特别明白为什么 它正在比较每个单词以查看它是否与行中的最后一个单词相同。)

def fixUppercase(doc):
    fixedText = ''
    for line in doc.split('\n'):
        line_words = line.split()   # Split the line once here.
        fixedLine = ''
        for word in line_words:
            if (
                word.isalpha()
                and (
                     word.isupper()
                     or word.istitle()
                     or word.islower()
                )
            ):
                if word == line_words[-1]:   # No need to split here.
                    fixedLine += word + '\n'
                else:
                    fixedLine += word + ' '
            elif (
                word.isalpha()
            ):
                lower = word.lower()
                if word == line_words[-1]:   # No need to split here.
                    fixedLine += lower + '\n'
                else:
                    fixedLine += lower + ' '
            else:
                if word == line_words[-1]:   # No need to split here.
                    fixedLine += word + '\n'
                else:
                    fixedLine += word + ' '
        fixedText += fixedLine

    return fixedText

在这里你可以看到我的时间安排。我从 Project Gutenberg 下载了“爱丽丝梦游仙境”作为测试输入。

annette@DISSONANCE:~/scratch$ wget 'https://www.gutenberg.org/files/11/11-0.txt' -O alice.txt
--2021-06-13 02:06:33--  https://www.gutenberg.org/files/11/11-0.txt
Resolving www.gutenberg.org (www.gutenberg.org)... 152.19.134.47, 2610:28:3090:3000:0:bad:cafe:47
Connecting to www.gutenberg.org (www.gutenberg.org)|152.19.134.47|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 174313 (170K) [text/plain]
Saving to: ‘alice.txt’

alice.txt                                               100%[============================================================================================================================>] 170.23K   175KB/s    in 1.0s

2021-06-13 02:06:35 (175 KB/s) - ‘alice.txt’ saved [174313/174313]

annette@DISSONANCE:~/scratch$ time python slow_ocr_cleanup.py --headings-last < alice.txt > alice1.txt

real    0m0.065s
user    0m0.047s
sys     0m0.016s
annette@DISSONANCE:~/scratch$ time python slow_ocr_cleanup.py --headings-first < alice.txt > alice2.txt
^CTraceback (most recent call last):
  File "slow_ocr_cleanup.py", line 117, in <module>
    main()
  File "slow_ocr_cleanup.py", line 106, in main
    doc = fixUppercase(doc)
  File "slow_ocr_cleanup.py", line 17, in fixUppercase
    if word == line.split()[-1]:
KeyboardInterrupt

real    0m16.856s
user    0m8.438s
sys     0m8.375s
annette@DISSONANCE:~/scratch!1$ time python slow_ocr_cleanup.py --fixed < alice.txt > alice3.txt

real    0m0.058s
user    0m0.047s
sys     0m0.000s

如您所见,在没有修复的情况下运行需要很长时间,所以我提前停止了它。

这是完整的测试程序:

import sys


def fixUppercase(doc):
    fixedText = ''
    for line in doc.split('\n'):
        fixedLine = ''
        for word in line.split():
            if (
                word.isalpha()
                and (
                     word.isupper()
                     or word.istitle()
                     or word.islower()
                )
            ):
                if word == line.split()[-1]:
                    fixedLine += word + '\n'
                else:
                    fixedLine += word + ' '
            elif (
                word.isalpha()
            ):
                lower = word.lower()
                if word == line.split()[-1]:
                    fixedLine += lower + '\n'
                else:
                    fixedLine += lower + ' '
            else:
                if word == line.split()[-1]:
                    fixedLine += word + '\n'
                else:
                    fixedLine += word + ' '
        fixedText += fixedLine

    return fixedText


def fixUppercaseFast(doc):
    fixedText = ''
    for line in doc.split('\n'):
        line_words = line.split()
        fixedLine = ''
        for word in line_words:
            if (
                word.isalpha()
                and (
                     word.isupper()
                     or word.istitle()
                     or word.islower()
                )
            ):
                if word == line_words[-1]:
                    fixedLine += word + '\n'
                else:
                    fixedLine += word + ' '
            elif (
                word.isalpha()
            ):
                lower = word.lower()
                if word == line_words[-1]:
                    fixedLine += lower + '\n'
                else:
                    fixedLine += lower + ' '
            else:
                if word == line_words[-1]:
                    fixedLine += word + '\n'
                else:
                    fixedLine += word + ' '
        fixedText += fixedLine

    return fixedText


def headingsFix(doc):
    fixedText = ''
    count = 0
    stopWords = ['on', 'and', 'of', 'as', 'for']
    for line in doc.split('\n'):
        tokenLine = ''
        for word in line.split():
            if word not in stopWords:
                tokenLine += word + " "
        if tokenLine.istitle() and (
            not line.endswith('.')
            and not line.endswith(',')
            and not line.endswith(')')
            and not line.endswith(';')
            and not line.endswith(':')
        ):

            count += 1
        else:
            fixedText += line

    return fixedText


def main():
    doc = sys.stdin.read()
    if '--headings-last' in sys.argv[1:]:
        doc = fixUppercase(doc)
        doc = headingsFix(doc)
    elif '--headings-first' in sys.argv[1:]:
        doc = headingsFix(doc)
        doc = fixUppercase(doc)
    elif '--fixed' in sys.argv[1:]:
        doc = headingsFix(doc)
        doc = fixUppercaseFast(doc)
    else:
        print('Specify --headings-last, --headings-first or --fixed', file=sys.stderr)
        sys.exit(1)
    print(doc, end='')


if __name__ == '__main__':
    main()

您会注意到字符串连接不是问题的根源,尽管它仍然是不可取的。在某些版本的 Python 中,有一种优化可以使它变得更快,但通常你不能总是依赖它来工作。这个question and answer更详细地解释了这个问题,但从广义上讲,重复使用++=在循环中构建越来越大的字符串效率低下,因为每次都需要复制整个字符串,而且它越来越长并且随着循环的进行而更长。这是一个臭名昭著的陷阱,称为Schlemiel the Painter's Algorithm。更好的选择是使用str.joinio.StringIO

【讨论】:

  • 感谢您的建议。我从来没有想过,如果它在标题中,每次迭代都会再次调用该方法。 (我会给你一个赞成票,但我的代表太低了。) 问:这与海象运算符有何关系?它不是在每次迭代时重新实例化 var 吗?
  • @theotechne 海象运算符是 Python 3.8 中使用 := 引入的赋值表达式的名称,恐怕我不知道它与您的问题有何关系。当您说“在标题中”或“在每次迭代中重新实例化 var”时,恐怕我真的不明白您的意思……您是在询问将单词分成一行,还是字符串连接,或者还有什么?
  • 哦,等等,您指的是条件句中的 'line.split()'s (if/elif/else)?抱歉,我想这是不编写这样的代码的另一个原因。我忘记了这些,并认为您指的是将 line.split() 分配给第一个块中的 var 而不是第二个块循环头。 (也感谢您向我展示了一个很好的可重现示例 - 仍在学习中。)
  • 啊,抱歉我没说清楚。是的,循环体内对line.split() 的调用是被反复调用的。
  • 我赞成你的回答,但是:“在这种情况下,CPython 有一个优化,它可以在你附加到字符串时应用”似乎不鼓励https://stackoverflow.com/questions/35787022/cython-string-concatenation-is-super-slow-what-else-does-it-do-poorly/35791033#35791033
【解决方案2】:

您的fixUppercase() 函数基本上是这样做的:

  • 将所有非全小写、正确标题或全大写的字母单词更改为全小写

但是,您假设文档仅包含 \n 作为空格,因此制表符(例如)会破坏您的代码。您可以改为使用正则表达式将文档分解为空间元字符和其余字符串。

您的主要问题是fixedUpper 效率低下造成的,因此解决方案是解决该问题。

这会做同样的事情,但更有效:

import re

example="""
This is an example.

It Has:\ta fEw examples of thIngs that should be FIXED and CHANGED!

Don't touch this: a123B or this_Is_finE

Did it woRk?
"""


def fixedUpper(doc):
    p = re.compile(r'\s|([^\s]+)')
    # go through all the matches and join them back together into a string when done
    return ''.join(
        # lowercase for any alphabetic substring that does not contain whitespace and isn't a title or all uppercase
        m.group(1).lower() if not (m.group(1) is None or m.group(1).istitle() or m.group(1).isupper()) and m.group(1).isalpha()
        # in all other cases, just leave the match untouched
        else m.group(0)
        for m in p.finditer(doc)
    )


print(repr(fixedUpper(example)))

输出(注意它如何保留空白):

"\nThis is an example.\n\nIt Has:\ta few examples of things that should be FIXED and CHANGED!\n\nDon't touch this: a123B or this_Is_finE\n\nDid it woRk?\n"

另请注意,这仍然存在您的代码也存在的问题:如果单词末尾有插入,则它不是固定的,例如woRk?

这样更好:

def fixedUpper(doc):
    p = re.compile(r'\s|((\w+)([^\w\s]*))')
    return ''.join(
        m.group(1).lower()
        if not (m.group(2) is None or m.group(2).istitle() or m.group(2).isupper()) and m.group(2).isalpha()
        else m.group(0)
        for m in p.finditer(doc)
    )

【讨论】:

  • 感谢这个正则表达式示例。它比我想出的更强大。 (您提到的一些循环逻辑问题不适用于我的具体项目,但对于更广泛的应用程序来说很重要,所以我很感激。)
猜你喜欢
  • 2020-04-12
  • 1970-01-01
  • 1970-01-01
  • 2014-01-29
  • 1970-01-01
  • 1970-01-01
  • 2016-12-31
  • 2021-07-25
  • 1970-01-01
相关资源
最近更新 更多