在Python 2.7中优化过滤列表

9

我需要多次筛选一个很大的列表,但我既关注代码的简洁性,又关注执行效率。举个例子:

all_things # huge collection of all things

# inefficient but clean code
def get_clothes():
    return filter(lambda t: t.garment, allThings)

def get_hats():
    return filter(lambda t: t.headgear, get_clothes())

我担心在迭代衣服列表时,实际上已经完成了迭代。我还想将两个过滤操作分开,因为它们属于两个不同的类,我不想在帽子类中重复第一个lambda函数。

# efficient but duplication of code
def get_clothes():
    return filter(lambda t: t.garment, allThings)

def get_hats():
    return filter(lambda t: t.headgear and t.garment, allThings)

我一直在研究生成器函数,因为它们似乎是正确的方法,但我还没有弄清楚如何使用。

如果你担心性能问题,你是否进行了性能测试? - Karl Knechtel
如果我认为它不明显的话,我早就这么做了。 - cammil
2
当涉及到性能时,“显然”是一个危险的词。 - Karl Knechtel
2
测试过了:经过我粗略的测试,其中衣服大约占整个列表的30%,执行时间减少了约40%。相当可靠。 - cammil
3个回答

25
首先,使用过滤器/lambda组合将被弃用。当前的函数式编程风格已在Python Functional Programming HOWTO中描述。
其次,如果你关心效率,而不是构建列表,你应该返回generators。在这种情况下,它们足够简单,可以使用generator expressions
def get_clothes():
    return (t for t in allThings if t.garment)

def get_hats():
    return (t for t in get_clothes() if t.headgear)

如果您更喜欢,也可以使用真正的生成器(据说更符合 Python 风格):
def get_clothes():
    for t in allThings:
       if t.garment:
           yield t

def get_hats():
    for t in get_clothes():
        if t.headgear:
            yield t

如果由于某种原因,有时您需要使用list而不是iterator,则可以通过简单的强制转换构造列表:
hats_list = list(get_hats())

请注意,上述代码不会构建衣服清单,因此效率与您的重复代码版本接近。

6
  1. “filter/lambda”组合并未被弃用。
  2. PEP 8建议不要返回一个生成器表达式——它们应在同一作用域下消耗,而不管它们是在何处创建的——应改用常规生成器。
  3. 如果需要一个列表,OP应该使用列表推导式,而不是将list包装在genexp周围。
- Raymond Hettinger
@RaymondHettinger:1)由于强烈反对,它尚未正式被废弃,但是已经考虑放弃它超过7年了。2)在PEP-8中没有找到任何相关的内容。3)仅当他始终需要列表时。 - vartec
@vartec:返回生成器表达式的一个危险示例:http://programmaticallyspeaking.com/?p=471。你的`get_hats`等应该是生成器本身(`for t... if t.headgear: yield t`),而不是返回生成器表达式。//看起来已经修复了。 - georg
@thg435:说得对,但正如我在我的答案中所说的,在这种特殊情况下,生成器表达式是可以的。在你提供的例子中,更多的问题是使用闭包,而不是闭包本身是一个生成器表达式。 - vartec
就我个人而言,有时我发现使用filter/map/reduce比等效的列表理解或循环更容易阅读。在本帖作者的情况下,使用列表理解会更简单、更易于阅读,但并非总是如此。 - Li-aung Yip
上面评论中的链接已经失效,但是可能应该引用http://programmaticallyspeaking.com/generator-combined-with-withusing-statement-python-vs-c.html。 - Donal Fellows

5

我正在寻找类似于对列表进行过滤的方法,但希望以稍微不同的格式呈现。

上面的get_hats()调用很好,但在重复使用方面有限制。我正在寻找更像get_hats(get_clothes(all_things))这样的东西,您可以指定一个源(all_things),然后使用尽可能少或尽可能多水平的过滤器get_hats()get_clothes()

我发现了一种使用生成器进行此操作的方法:

def get_clothes(in_list):
    for item in in_list:
        if item.garment:
            yield item

def get_hats(in_list):
    for item in in_list:
        if item.headgear:
            yield item

这可以通过以下方式调用:

get_hats(get_clothes(all_things))

我测试了原始解决方案、vartec的解决方案和这个额外的解决方案,以查看效率,并对结果感到有些惊讶。代码如下:

设置:

class Thing:
    def __init__(self):
        self.garment = False
        self.headgear = False

all_things = [Thing() for i in range(1000000)]

for i, thing in enumerate(all_things):
    if i % 2 == 0:
        thing.garment = True
    if i % 4 == 0:
        thing.headgear = True

原始解决方案:

def get_clothes():
    return filter(lambda t: t.garment, all_things)

def get_hats():
    return filter(lambda t: t.headgear, get_clothes())

def get_clothes2():
    return filter(lambda t: t.garment, all_things)

def get_hats2():
    return filter(lambda t: t.headgear and t.garment, all_things)

我的解决方案:

def get_clothes3(in_list):
    for item in in_list:
        if item.garment:
            yield item

def get_hats3(in_list):
    for item in in_list:
        if item.headgear:
            yield item

vartec的解决方案:

def get_clothes4():
    for t in all_things:
       if t.garment:
           yield t

def get_hats4():
    for t in get_clothes4():
        if t.headgear:
            yield t

计时代码:

import timeit

print 'get_hats()'
print timeit.timeit('get_hats()', 'from __main__ import get_hats', number=1000)

print 'get_hats2()'
print timeit.timeit('get_hats2()', 'from __main__ import get_hats2', number=1000)

print '[x for x in get_hats3(get_clothes3(all_things))]'
print timeit.timeit('[x for x in get_hats3(get_clothes3(all_things))]',
                    'from __main__ import get_hats3, get_clothes3, all_things',
                    number=1000)

print '[x for x in get_hats4()]'
print timeit.timeit('[x for x in get_hats4()]',
                    'from __main__ import get_hats4', number=1000)

结果:

get_hats()
379.334653854
get_hats2()
232.768362999
[x for x in get_hats3(get_clothes3(all_things))]
214.376812935
[x for x in get_hats4()]
218.250688076

生成器表达式似乎略微更快,我和vartec的解决方案之间的时间差可能只是噪音。但我更喜欢能够以任何顺序应用所需的任何过滤器的灵活性。

4

一遍完成的方法(伪代码):

clothes = list()
hats = list()
for thing in things:
    if thing is a garment:
        clothes.append(thing)
        if thing is a hat:
            hats.append(thing)

一次大规模的操作和一次较小规模的操作(列表推导式):
clothes = [ x for x in things if x is garment ]
hats = [ x for x in clothes if x is hat ]

如果你想创建整个列表,使用生成器表达式进行惰性计算就没有意义,因为你不会懒惰。如果你一次只想处理几件事情,或者内存受限,请使用@vartec的生成器解决方案。

1
你可能需要修复things中的thing使用 - okm
@okm:抱歉,我没有看到它 - 你能详细说明一下吗? - Li-aung Yip
我的意思是,如果“thing”是帽子,“[thing in clothes if thing is hat]”就不是语法正确的,对吧? - okm

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