Применение нескольких фильтров к списку кортежей
Я ищу эффективный питонический способ применения нескольких фильтров к списку кортежей.
В качестве примера предположим, что такие фильтры:
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