Применение нескольких фильтров к списку кортежей

Я ищу эффективный питонический способ применения нескольких фильтров к списку кортежей.

В качестве примера предположим, что такие фильтры:

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-records):

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 в документах, но все ссылки мертвы.

Кроме того, поскольку я планирую использовать это на довольно больших наборах данных и, возможно, с большим количеством фильтров в веб-службе производства, он должен быть эффективным, и я не могу сказать, является ли это решение.

Любые предложения или улучшения приветствуются.

Ответы

Ответ 1

Улучшение: заменить рекурсию на итерацию

На самом деле нет "составной функции для произвольного списка функций"; однако довольно легко построить цепочку фильтров с помощью простой петли:

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

Улучшение: заказывайте фильтры по ограничению и скорости

Цепные итераторы работают так быстро, что общее время работы будет зависеть от вызовов предикатных функций.

Лучший результат может быть получен при заказе предикатов для минимизации общей работы. В общем, лучше провести дешевые тесты перед дорогостоящими испытаниями и поставить более ограничительные тесты перед тестами, которые не отфильтровывают многие случаи.

Пример

В этом примере предикаты имеют одинаковую стоимость (вызов функции, индексирование кортежей и сравнение с константой), но они различаются по степени ограниченности (фильтры t[2]==4 исключают 80% случаев, а t[0]>1 и t[1]<3 каждый фильтрует только 40% данных).

>>> 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)

Заметки, поднятые из комментариев

  • Функции фильтра выполняют нулевые приложения предиката, когда входной итерабельный пуст. Это похоже на выполнение цикла для пустого списка.

  • Каждый фильтр уменьшает количество данных, подаваемых в охватывающий фильтр. Соответственно, каждый фильтр получает только привязки к данным, которые прошли его через предыдущие фильтры.

  • Не беспокойтесь о lambda в этом примере. Он выполняет ту же функцию, что и обычный def. Это просто удобный способ записи списка фильтров.

  • В Python 3 была обновлена ​​функция filter(), чтобы вернуть итератор вместо списка. В Python 2 вы можете добиться того же эффекта, используя itertools.ifilter() вместо фильтр().

Ответ 2

Вы ищете что-то вроде этого?

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

Это имеет то преимущество, что как только один фильтр возвращает False, этот элемент списка не будет включен.

Ответ 3

Выражение генератора кажется самым идиоматическим (и вы получаете ленту бесплатно):

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

Ответ 4

Я рекомендую использовать следующий шаблон для простого применения серии/цепи фильтров на генераторах:

from functools import reduce, partial
from itertools import ifilter

filtered = reduce(lambda s,f: ifilter(f,s), filter_set, unfiltered)

В двух словах он устанавливает цепочку фильтров слева направо на генераторе и возвращает генератор, который является результатом применения всех фильтров на оригинале.

Если вы хотите получить список, достаточно:

[reduce(lambda s,f: ifilter(f,s), (f1,f2,f3,), tuples)]

и если вы хотите получить одну функцию, вы можете определить ее как:

chain_filters = partial(reduce, lambda s,f: ifilter(f,s))

и используйте как:

[chain_filters((f1,f2,f3,), tuples)]

Обратите внимание, что это решение не содержит фильтры (как в all()), а цепочки. Если вы используете тяжелые вычисления, вам нужно поставить более агрессивный фильтр в начало цепи, например. фильтр цветка перед фильтром запросов базы данных и т.д.

Ответ 5

похож на @Раймонд Хеттингер,

Хотя, я предлагаю использовать ifilter из itertools в качестве генератора.

from itertools import ifilter

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

Ответ 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

Результат:

original 7.23815271085
filter_lambda_all 14.1629812265
loop 7.23445844453
pop_loop 7.3084566637
with_ifilter 9.2767674205
for_loop 7.02854999945