简化/优化for循环链

时间:2016-07-17 17:28:30

标签: python dictionary filter nested-loops reduce

我有一系列for循环,它们在原始的字符串列表上工作,然后逐渐过滤掉链中的列表,例如:

import re

# Regex to check that a cap exist in string.
pattern1 = re.compile(r'\d.*?[A-Z].*?[a-z]')
vocab = ['dog', 'lazy', 'the', 'fly'] # Imagine it's a longer list.

def check_no_caps(s):
    return None if re.match(pattern1, s) else s

def check_nomorethan_five(s):
    return s if len(s) <= 5 else None

def check_in_vocab_plus_x(s,x):
    # s and x are both str.
    return None if s not in vocab else s+x

slist = ['the', 'dog', 'jumps', 'over', 'the', 'fly']
# filter with check_no_caps
slist = [check_no_caps(s) for s in slist]
# filter no more than 5.
slist = [check_nomorethan_five(s) for s in slist if s is not None]
# filter in vocab
slist = [check_in_vocab_plus_x(s, str(i)) for i,s in enumerate(slist) if s is not None]

以上只是一个例子,实际上我操作字符串的函数更复杂,但它们会返回原始字符串或操作字符串。

我可以使用生成器而不是列表,并执行以下操作:

slist = ['the', 'dog', 'jumps', 'over', 'the', 'fly']
# filter with check_no_caps and no more than 5.
slist = (s2 check_no_caps(s1) for s1 in slist 
         for s2 in check_nomorethan_five(s1) if s1)
# filter in vocab
slist = [check_in_vocab_plus_x(s, str(i)) for i,s in enumerate(slist) if s is not None]

或者在一个疯狂的嵌套生成器中:

slist = ['the', 'dog', 'jumps', 'over', 'the', 'fly']
slist = (s3 check_no_caps(s1) for s1 in slist 
         for s2 in check_nomorethan_five(s1) if s1
         for s3 in check_in_vocab_plus_x(s2, str(i)) if s2)

必须有更好的方法。 有没有办法让for循环链变得更快?

有没有办法通过mapreducefilter来实现?它会更快吗?

想象一下,我原来的滑板非常大,就像数十亿。而且我的函数不像上面的函数那么简单,它们进行一些计算并且每秒执行大约1,000次调用。

6 个答案:

答案 0 :(得分:7)

首先是你对字符串的整个过程。你正在采取一些字符串,并为每个字符串应用某些功能。然后清理列表。让我们说一段时间,你应用于字符串的所有函数都在一个恒定的时间工作(它不是真的,但现在它不重要)。在您的解决方案中,您使用一个函数(即O(N))迭代throgh列表。然后你接下一个函数并再次迭代(另一个O(N)),依此类推。因此,加速的显而易见的方法是减少循环次数。这并不困难。

接下来要做的是尝试优化您的功能。例如。你使用regexp检查字符串是否有大写字母,但是有str.islower(如果字符串中的所有套接字符都是小写且至少有一个套接字符,则返回true,否则返回false。)

因此,这是第一次简化和加速代码的尝试:

vocab = ['dog', 'lazy', 'the', 'fly'] # Imagine it's a longer list.

# note that first two functions can be combined in one
def no_caps_and_length(s):
    return s if s.islower() and len(s)<=5 else None

# this one is more complicated and cannot be merged with first two
# (not really, but as you say, some functions are rather complicated)
def check_in_vocab_plus_x(s,x):
    # s and x are both str.
    return None if s not in vocab else s+x

# now let's introduce a function that would pipe a string through all functions you need
def pipe_through_funcs(s):
    # yeah, here we have only two, but could be more
    funcs = [no_caps_and_length, check_in_vocab_plus_x]
    for func in funcs:
        if s == None: return s
        s = func(s)
    return s

slist = ['the', 'dog', 'jumps', 'over', 'the', 'fly']
# final step:
slist = filter(lambda a: a!=None, map(pipe_through_funcs, slist))

可能还有一件事可以改进。目前,您遍历列表修改元素,然后将其过滤掉。但是如果过滤然后修改可能会更快。像这样:

vocab = ['dog', 'lazy', 'the', 'fly'] # Imagine it's a longer list.

# make a function that does all the checks for filtering
# you can make a big expression and return its result,
# or a sequence of ifs, or anything in-between,
# it won't affect performance,
# but make sure you put cheaper checks first
def my_filter(s):
    if len(s)>5: return False
    if not s.islower(): return False
    if s not in vocab: return False
    # maybe more checks here
    return True

# now we need modifying function
# there is a concern: if you need indices as they were in original list
# you might need to think of some way to pass them here
# as you iterate through filtered out list
def modify(s,x):
    s += x
    # maybe more actions
    return s

slist = ['the', 'dog', 'jumps', 'over', 'the', 'fly']
# final step:
slist = map(modify, filter(my_filter, slist))

另请注意,在某些情况下,生成器,地图和事物可以更快,但并非总是如此。我相信,如果你过滤掉的项目数量很大,那么使用附加的for循环可能会更快。我不会保证它会更快但你可以尝试这样的事情:

initial_list = ['the', 'dog', 'jumps', 'over', 'the', 'fly']
new_list = []
for s in initial_list:
    processed = pipe_through_funcs(s)
    if processed != None: new_list.append(processed)

答案 1 :(得分:3)

如果您将转换功能统一起来,那么您可以执行以下操作:

import random
slist = []
for i in range(0,100):
    slist.append(random.randint(0,1000))

# Unified functions which have the same function description
# x is the value
# i is the counter from enumerate
def add(x, i):
    return x + 2

def replace(x, i):
    return int(str(x).replace('2', str(i)))

# Specifying your pipelines as a list of tuples 
# Where tuple is (filter function, transformer function)
_pipeline = [
    (lambda s: True, add),
    (lambda s: s % 2 == 0, replace),
]

# Execute your pipeline
for _filter, _fn in _pipeline:
    slist = map(lambda item: _fn(*item), enumerate(filter(_filter, slist)))

代码适用于python 2和python 3.不同之处在于Python3中的所有东西都返回一个生成器,因此它必须先执行才能执行。因此,您可以有效地对列表进行一次迭代。

print(slist)
<map object at 0x7f92b8315fd0>

然而,迭代一次或多次不会产生很大的差别,只要它可以在内存中完成,因为无论迭代方法如何,都必须执行相同数量的转换和过滤。因此,为了改进代码,请尝试尽可能快地进行过滤和转换函数。

例如@Rawing提到将词汇作为一个集合而不是列表将会产生很大的不同,特别是对于大量的项目。

答案 2 :(得分:3)

你有一堆检查可以用来进行迭代:

def check1(s):
    if s.islower():
        return s

def check2(s):
    if len(s) < 5:
        return s

checks = [check1, check2]

可迭代的字符串:

l = ['dog', 'Cat', 'house', 'foo']

所以一个问题是你是应该首先迭代检查还是首先串起来。

def checks_first(l, checks):
    for check in checks:
        l = filter(None, map(check, l))

    return list(l)


def strings_first(l, checks):
    res = []

    for item in l:
        for check in checks:
            item = check(item)
            if item is None:
                break
        else:
            res.append(item)

    return res

您可以使用timeit module计算这两种方法。注意:您可能必须使用字符串的子集来及时获得这些结果。

import timeit

print(timeit.timeit('checks_first(l, checks)', setup='from __main__ import checks_first, checks, l', number=10))
print(timeit.timeit('strings_first(l, checks)', setup='from __main__ import strings_first, checks, l', number=10))

哪个更快取决于数字检查与数字字符串,硬件等的比率。从我完成的测试中,它们似乎以大致相同的速度运行。

我的猜测是,通过优化一些检查可以节省最多的时间。一个很好的起点是确定花费最多时间的支票。这可以使用闭包来完成检查功能。

import functools

def time_func(func, timer_dict):

    @functools.wraps(func)
    def inner(*args, **kwargs):
        t0 = time.time()
        res = func(*args, **kwargs)
        timer_dict[func.__name__] += time.time() - t0
        return res

    return inner

将此项应用于支票:

from collections import defaultdict

timer_dict = defaultdict(lambda: 0)
checks = [time_func(check, timer_dict) for check in checks]

然后调用应用检查的函数并查看timer_dict的时间信息。

checks_first(l, checks)
strings_first(l, checks)

print(dict(timer_dict))

# {'check1': 0.41464924812316895, 'check2': 0.2684309482574463}

然后通过检查或分析确定昂贵检查中的瓶颈。后者可以通过使用time module计时行代码或使用类似this的行分析器来完成。

优化您的算法和数据结构以摆脱这些瓶颈。您可以查看Cython以获取需要带到(接近)C速度的代码。

答案 3 :(得分:2)

首先:我认为您的示例代码没有按照您的想法进行。结果是['the0', 'dog1', None, None, 'the4', 'fly5'],但我相信您不需要None值。

唯一合理的答案是衡量你的表现并找出瓶颈,这可能是你的检查功能,而不是外部循环。

从检查函数外部,我看到的唯一真正的优化是执行将首先减少集合的检查,以便在以下迭代中使用较小的循环,并减少对值执行的检查数量反正会放弃。根据您的数据和在第一次检查中丢弃的值的数量,您可能会看到性能大幅提升......或者不是!

真正了解的唯一方法是分析您的代码。你应该cProfileRunSnakeRun一起使用并解决你的瓶颈问题,否则你最终会选择错误的东西。

要对脚本进行概要分析,您可以按如下方式运行它: python -m cProfile <script-name>

答案 4 :(得分:2)

我可以看到你可能做出的三个优化。首先,如果“词汇”中的所有单词都小于或等于5,则不需要检查“slist”中的单词是否小于或等于5,这意味着您可以删除整个for循环。第二个优化是,如果“vocab”中的所有单词都只是小写,并且您的单词比较算法区分大小写,那么您不需要检查“slist”中的单词是否区分大小写,这意味着您可以删除for loop。

这个原则的基本概括是,如果一个单词必须满足几个条件而一个条件意味着另一个条件(即如果你需要一个可被4和2整除的数字,你只需要检查它是否可以被4整除。) ,你可以删除隐含条件。

如果“vocab”的单词长度超过五个字母或带大写字母的单词,您应该可以将它们从“vocab”中删除,因为“slist”中大写或超过五个字母的所有单词都能够在获得词汇之前从你的支票中删除。

最后一个优化是确定“slist”中的单词是否在“词汇”中与找到它们的交集相同。有许多相对快速的算法可以做到这一点,不需要for循环。以下是一些例子:

Efficient list intersection algorithm

Computing set intersection in linear time?

总之,你可以删除两个for循环,并减少循环的“词汇” - “slist”比较的时间复杂性。

答案 5 :(得分:1)

优化很大程度上取决于具体的代码。如果不知道字符串上的实际操作和数据的性质,则有效结果的可能性很小。此外,OP特别将字符串操作描述为“更复杂”。这样可以减少一般性能中的外部循环部分。

我可以在这里添加两个相关且简单的提示,其中包括使用内置函数调用和生成器进行优化:

  1. 内置函数可提高性能,从而在本机代码中保持更多工作。当您使用它们来调用lambda或其他python调用时,它们会失去大部分性能值。只使用本机内置函数完成任务时使用它们。 itertoolsoperatorfunctools模块可以为此提供很多帮助。
  2. 生成器主要帮助进行内存优化,您不希望一次将所有值保存在内存中。如果你可以在一次迭代中完成所有操作而不使用它们,那么它会更好并节省生成器开销。
  3. 我在具体示例中改变的另一件事是使用正则表达式。在这个大写字母的简单情况下,只需比较字符就可以快速进行。定期完成不是很好的表现可能会对表现造成危险,我倾向于避免它们在没有特定利益的情况下进行更复杂的比较。

    vocab = ['dog', 'lazy', 'the', 'fly'] # Imagine it's a longer list.
    
    def check_no_caps(s):
        for char in s:
            if 'A' <= char <= 'Z':
                return None
        return s
    
    def check_nomorethan_five(s):
        return s if len(s) <= 5 else None
    
    def check_in_vocab_plus_x(s, x):
        # s and x are both str.
        return None if s not in vocab else s + str(x)
    
    slist = ['the', 'dog', 'jumps', 'over', 'the', 'fly']
    
    result = [check_in_vocab_plus_x(check_nomorethan_five(check_no_caps(string)), i) for i, string in enumerate(slist)]