在Python中构建高效的函数

时间:2015-08-06 18:01:52

标签: python

我的编程几乎都是自学成才,所以如果我的一些术语在这个问题上没有,我会提前道歉。另外,我将使用一个简单的例子来帮助说明我的问题,但请注意,示例本身并不重要,它只是一种希望让我的问题更清晰的方法。

想象一下,我有一些格式不正确的文本,有很多额外的空白区域,我想要清理。因此,我创建了一个函数,它将替换任何具有新行字符的空白字符组,其中包含一个新行字符和任何其他具有单个空格的空白字符组。该函数可能如下所示

def white_space_cleaner(text):
    new_line_finder = re.compile(r"\s*\n\s*")
    white_space_finder = re.compile(r"\s\s+")
    text = new_line_finder.sub("\n", text)
    text = white_space_finder.sub(" ", text)
    return text

这很好用,问题是现在每次调用函数时都要编译正则表达式。为了让它运行得更快我可以像这样重写它

new_line_finder = re.compile(r"\s*\n\s*")
white_space_finder = re.compile(r"\s\s+")
def white_space_cleaner(text):
    text = new_line_finder.sub("\n", text)
    text = white_space_finder.sub(" ", text)
    return text

现在正则表达式只编译一次,函数运行得更快。在两个函数上使用timeit我发现第一个函数每个循环需要27.3μs,第二个函数每个循环需要25.5μs。一个小的加速,但如果函数被称为数百万的时间或有数百个模式而不是2,这可能是重要的。当然,第二个函数的缺点是它污染全局命名空间并使代码可读性降低。是否有一些“Pythonic”方法将一个对象(如编译的正则表达式)包含在函数中,而不是每次调用函数时都重新编译它?

5 个答案:

答案 0 :(得分:5)

保留要应用的元组(正则表达式和替换文本)列表;似乎并没有迫切需要单独命名每一个。

finders = [
    (re.compile(r"\s*\n\s*"), "\n"),
    (re.compile(r"\s\s+"), " ")
]
def white_space_cleaner(text):
    for finder, repl in finders:
        text = finder.sub(repl, text)
    return text

您还可以合并functools.partial

from functools import partial
replacers = {
    r"\s*\n\s*": "\n",
    r"\s\s+": " "
}
# Ugly boiler-plate, but the only thing you might need to modify
# is the dict above as your needs change.
replacers = [partial(re.compile(regex).sub, repl) for regex, repl in replacers.iteritems()]


def white_space_cleaner(text):
    for replacer in replacers:
        text = replacer(text)
    return text

答案 1 :(得分:3)

另一种方法是在一个类中对通用功能进行分组:

class ReUtils(object):
    new_line_finder = re.compile(r"\s*\n\s*")
    white_space_finder = re.compile(r"\s\s+")

    @classmethod
    def white_space_cleaner(cls, text):
        text = cls.new_line_finder.sub("\n", text)
        text = cls.white_space_finder.sub(" ", text)
        return text

if __name__ == '__main__':
   print ReUtils.white_space_cleaner("the text")

它已经分组在一个模块中,但根据代码的其余部分,一个类也适用。

答案 2 :(得分:2)

您可以将正则表达式编译放入函数参数中,如下所示:

def white_space_finder(text, new_line_finder=re.compile(r"\s*\n\s*"),
                             white_space_finder=re.compile(r"\s\s+")):
    text = new_line_finder.sub("\n", text)
    text = white_space_finder.sub(" ", text)
    return text

由于默认函数参数被评估when the function is parsed,它们只会被加载一次而它们不会在模块命名空间中。如果你真的需要,它们还可以灵活地替换调用代码。缺点是有些人可能会认为它污染了功能签名。

我想尝试计时,但我无法弄清楚如何正确使用timeit。您应该看到与全局版本类似的结果。

马库斯对你的帖子的评论是正确的;有时将变量放在模块级别是很好的。但是,如果您不希望其他模块容易看到它们,请考虑使用下划线添加名称;这个marks them as module-private,如果你from module import *,它将不会导入以下划线开头的名称(如果你通过名字询问它们,你仍然可以得到它们)。

永远记住;最终所有“在Python中执行此操作的最佳方式”几乎总是“使代码最具可读性的原因是什么?”首先创建Python是为了易于阅读,所以做你认为最具可读性的东西。

答案 3 :(得分:1)

在这种特殊情况下,我认为无关紧要。检查:

Is it worth using Python's re.compile?

正如您在答案和源代码中所看到的那样:

https://github.com/python/cpython/blob/master/Lib/re.py#L281

re模块的实现具有正则表达式本身的缓存。因此,您看到的小速度可能是因为您避免查找缓存。

现在,正如问题一样,有时候做这样的事情是非常相关的,就像建立一个内部缓存一样,它仍然是函数的命名空间。

def heavy_processing(arg):
    return arg + 2

def myfunc(arg1):
    # Assign attribute to function if first call
    if not hasattr(myfunc, 'cache'):
        myfunc.cache = {}

    # Perform lookup in internal cache
    if arg1 in myfunc.cache:
        return myfunc.cache[arg1]

    # Very heavy and expensive processing with arg1
    result = heavy_processing(arg1)
    myfunc.cache[arg1] = result
    return result

这样执行:

>>> myfunc.cache
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'cache'
>>> myfunc(10)
12
>>> myfunc.cache
{10: 12}

答案 4 :(得分:-1)

您可以使用静态函数属性来保存已编译的re。此示例执行类似操作,将转换表保留在一个函数属性中。

def static_var(varname, value):
    def decorate(func):
        setattr(func, varname, value)
        return func
    return decorate

@static_var("complements", str.maketrans('acgtACGT', 'tgcaTGCA'))
def rc(seq):
    return seq.translate(rc.complements)[::-1]