21

タプルのリストに複数のフィルターを適用する効率的で Pythonic な方法を探しています。

例として、次のようなフィルターを想定します。

def f1(t): return t[3]<10
def f2(t): return t[0]!=1
def f3(t): return t[1] in ("lisa","eric")
def f4(t): return t[3]>2

そして、次のような n タプル (つまり、db レコード):

tuples=[
(0,'tom','...',8),
(1,'john','...',17),
(2,'lisa','...',1),
(3,'eric','...',18)
]

以下の作品:

def nFilter(filters,tuples):
    if filters and tuples:
        return nFilter(filters,filter(filters.pop(),tuples))
    else: return tuples

次のような結果が得られます。

>>> nFilter([f1,f2,f3],tuples)
[(2, 'lisa', '...', 1)]

>>> nFilter([f1,f2,f3,f4],tuples)
[]

しかし、もっと直接的な方法があるかどうか疑問に思っています。f1(f2(...fn(tuples)...))私が念頭に置いていたのは、関数の任意のリストに対する関数合成 (つまり ) のようなものです。ドキュメントには関数を含む関数ライブラリへの参照がありcomposeますが、リンクはすべて無効です。

また、かなり大きなデータ セットでこれを使用することを計画しており、おそらく実稼働 Web サービスで多数のフィルターを使用する予定であるため、効率的である必要があり、このソリューションが有効かどうかはわかりません。

提案や改善は大歓迎です。

4

7 に答える 7

32

改善: 再帰を反復に置き換えます

「関数の任意のリストの構成関数」は実際にはありません。ただし、単純な for ループを使用してフィルタ チェーンを構築するのは非常に簡単です。

def nFilter(filters, tuples):
    for f in filters:
        tuples = filter(f, tuples)
    return tuples

改善: 制限と速度によるフィルターの並べ替え

連鎖イテレータは非常に高速であるため、総実行時間は述語関数の呼び出しによって支配される傾向があります。

述語を順序付けして総作業量を最小化することにより、最良の結果を得ることができます。一般に、高価なテストの前に安価なテストを配置し、多くのケースを除外しないテストの前に、より制限的なテストを配置することをお勧めします。

この例では、述語のコストはほぼ同じ (関数呼び出し、タプル インデックス作成、および定数との比較) ですが、制限の程度が異なります ( andそれぞれは 40 のみを除外するのt[2]==4に対し、各述語は 80% を除外します)。データの %)。t[0]>1t[1]<3

>>> from itertools import product

>>> filters = [lambda t: t[2]==4, lambda t: t[0]>1, lambda t: t[1]<3]
>>> for tup in nFilter(filters, product(range(5), repeat=3)):
        print(tup)

(2, 0, 4)
(2, 1, 4)
(2, 2, 4)
(3, 0, 4)
(3, 1, 4)
(3, 2, 4)
(4, 0, 4)
(4, 1, 4)
(4, 2, 4)

コメントから巻き上げられたメモ

  • フィルター関数は、入力 iterable が空の場合、述語の適用をゼロにします。空のリストに対して for ループを実行するようなものです。

  • 各フィルターは、囲んでいるフィルターに供給されるデータの量を減らします。したがって、各フィルターは、前のフィルターを通過したデータにのみ適用されます。

  • 例の については心配しないでくださいlambda。通常の と同じ機能を果たしますdef。これは、フィルタのリストを記述する便利な方法です。

  • Python 3 では、filter()関数が更新され、リストではなく反復子が返されました。Python 2 では、 filter () の代わりにitertools.ifilter()を使用して同じ効果を得ることができます。

于 2012-09-12T10:43:23.813 に答える
27

このようなものをお探しですか?

filters = (f1,f2,f3,f4)
filtered_list = filter( lambda x: all(f(x) for f in filters), your_list )

これには、単一のフィルターが を返すとすぐにFalse、そのリスト要素が含まれないという利点があります。

于 2012-09-12T10:43:52.390 に答える
6

ジェネレーター式は、最も慣用的なアプローチのようです (そして、無料で怠惰になります)。

def nFilter(filters, tuples):
    return (t for t in tuples if all(f(t) for f in filters))

または同等の(そしておそらくより読みやすい):

def nFilter(filters, tuples):
    for tuple in tuples:
        if all(filter(tuple) for filter in filters):
            yield tuple
于 2012-09-12T11:11:37.363 に答える
6

まあ、ここには派手な itertools などはありません。単純なループを使用して、再帰とジェネレータのオーバーヘッドを回避するだけです。

def for_loop(filters, tuples):
    for f in filters:
        tuples = filter(f, tuples)
        if not tuples: 
            return tuples
    return tuples

ここに少し汚れたベンチマークがあります:

import datetime
from itertools import ifilter
from timeit import Timer

def f1(t): return t[3]<10
def f2(t): return t[0]!=1
def f3(t): return t[1] in ("lisa","eric")
def f4(t): return t[3]>2

def original(filters,tuples):
    if filters and tuples:
        return original(filters,filter(filters.pop(),tuples))
    else: 
        return tuples

def filter_lambda_all(filters, tuples):
    return filter(lambda t: all(f(t) for f in filters), tuples)

def loop(filters, tuples):
    while filters and tuples:
        f = filters[0]
        del filters[0]
        tuples = filter(f, tuples)
    return tuples

def pop_loop(filters, tuples):
    while filters and tuples:
        tuples = filter(filters.pop(), tuples)
    return tuples

def for_loop(filters, tuples):
    for f in filters:
        tuples = filter(f, tuples)
        if not tuples: 
            return tuples
    return tuples


def with_ifilter(filters, tuples):
    for f in filters:
        tuples = ifilter(f, tuples)
    return tuples

_filters = [f1, f2, f3, f4]

def time(f):
    def t():
        return [    (0,'tom','...',8),
                    (1,'john','...',17),
                    (2,'lisa','...',1),
                    (3,'eric','...',18)
                ]*1000
    for i in xrange(4):
        list(f(_filters[i:] * 15,t()))

if __name__=='__main__':
    for f in (original,filter_lambda_all,loop,pop_loop,with_ifilter,for_loop):
        t = Timer(lambda: time(f))
        d = t.timeit(number=400)
        print f.__name__, d

結果:

オリジナル 7.23815271085
filter_lambda_all 14.1629812265
ループ 7.23445844453
pop_loop 7.3084566637
with_ifilter 9.2767674205
for_loop 7.02854999945

于 2012-09-12T11:11:58.233 に答える
4

@Raymond Hettingerに似て、

ただし、itertools の ifilter をジェネレーターとして使用することをお勧めします。

from itertools import ifilter

def nFilter(filters,tuples):
      return ifilter(lambda t: all(f(t) for f in filters), tuples)
于 2012-09-12T10:46:50.910 に答える