Python: разделить список на основе условия?
каков наилучший способ, как эстетически, так и с точки зрения производительности, разделить список элементов на несколько списков на основе условного? Эквивалент:
good = [x for x in mylist if x in goodvals]
bad = [x for x in mylist if x not in goodvals]
есть ли более элегантный способ сделать это?
обновление: вот реальный сценарий использования, чтобы лучше объяснить, что я пытаюсь сделать:
# files looks like: [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi'), ... ]
IMAGE_TYPES = ('.jpg','.jpeg','.gif','.bmp','.png')
images = [f for f in files if f[2].lower() in IMAGE_TYPES]
anims = [f for f in files if f[2].lower() not in IMAGE_TYPES]
27 ответов:
good = [x for x in mylist if x in goodvals] bad = [x for x in mylist if x not in goodvals]есть ли более элегантный способ сделать это?
этот код прекрасно читается, и предельно ясно!
# files looks like: [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi'), ... ] IMAGE_TYPES = ('.jpg','.jpeg','.gif','.bmp','.png') images = [f for f in files if f[2].lower() in IMAGE_TYPES] anims = [f for f in files if f[2].lower() not in IMAGE_TYPES]опять же, это отлично!
могут быть небольшие улучшения производительности с использованием наборов, но это тривиальная разница, и я считаю, что понимание списка намного легче читать, и вам не нужно беспокоиться о том, что порядок будет испорчен, дубликаты будут удалены, как и так далее.
In факт, я могу сделать еще один шаг "назад", и просто использовать простой цикл for:
images, anims = [], [] for f in files: if f.lower() in IMAGE_TYPES: images.append(f) else: anims.append(f)список-понимание или использование
set()это нормально, пока вам не нужно добавить какую - то другую проверку или другой бит логики-скажем, вы хотите удалить все 0-байтовые jpeg, вы просто добавляете что-то вроде..if f[1] == 0: continue
вот ленивый подход итератора:
from itertools import tee def split_on_condition(seq, condition): l1, l2 = tee((condition(item), item) for item in seq) return (i for p, i in l1 if p), (i for p, i in l2 if not p)он оценивает условие один раз на элемент и возвращает два генератора, первый из которых дает значения из последовательности, где условие истинно, а другой, где оно ложно.
потому что это лениво, вы можете использовать его на любом итераторе, даже бесконечном:
from itertools import count, islice def is_prime(n): return n > 1 and all(n % i for i in xrange(2, n)) primes, not_primes = split_on_condition(count(), is_prime) print("First 10 primes", list(islice(primes, 10))) print("First 10 non-primes", list(islice(not_primes, 10)))обычно, хотя не ленивый подход к возврату списка лучше:
def split_on_condition(seq, condition): a, b = [], [] for item in seq: (a if condition(item) else b).append(item) return a, bEdit: для вашего более конкретного использования расщепления элементы в разные списки по некоторому ключу, вот общая функция, которая делает это:
DROP_VALUE = lambda _:_ def split_by_key(seq, resultmapping, keyfunc, default=DROP_VALUE): """Split a sequence into lists based on a key function. seq - input sequence resultmapping - a dictionary that maps from target lists to keys that go to that list keyfunc - function to calculate the key of an input value default - the target where items that don't have a corresponding key go, by default they are dropped """ result_lists = dict((key, []) for key in resultmapping) appenders = dict((key, result_lists[target].append) for target, keys in resultmapping.items() for key in keys) if default is not DROP_VALUE: result_lists.setdefault(default, []) default_action = result_lists[default].append else: default_action = DROP_VALUE for item in seq: appenders.get(keyfunc(item), default_action)(item) return result_listsиспользование:
def file_extension(f): return f[2].lower() split_files = split_by_key(files, {'images': IMAGE_TYPES}, keyfunc=file_extension, default='anims') print split_files['images'] print split_files['anims']
проблема со всеми предлагаемыми решениями заключается в том, что он будет сканировать и применять функцию фильтрации дважды. Я бы сделал простую небольшую функцию, как это:
def SplitIntoTwoLists(l, f): a = [] b = [] for i in l: if f(i): a.append(i) else: b.append(i) return (a,b)таким образом, вы не обрабатываете ничего дважды, а также не повторяете код.
сначала пойти (pre-OP-edit): использовать наборы:
mylist = [1,2,3,4,5,6,7] goodvals = [1,3,7,8,9] myset = set(mylist) goodset = set(goodvals) print list(myset.intersection(goodset)) # [1, 3, 7] print list(myset.difference(goodset)) # [2, 4, 5, 6]это хорошо как для читаемости (IMHO), так и для производительности.
второй раз (после операции редактирования):
создайте свой список хороших расширений в виде набора:
IMAGE_TYPES = set(['.jpg','.jpeg','.gif','.bmp','.png'])и это увеличит производительность. В противном случае, то, что у вас есть, выглядит хорошо для меня.
Мне в основном нравится подход Андерса, поскольку он очень общий. Вот версия, которая сначала помещает классификатор (чтобы соответствовать синтаксису фильтра) и использует defaultdict (предполагаемый импортированный).
def categorize(func, seq): """Return mapping from categories to lists of categorized items. """ d = defaultdict(list) for item in seq: d[func(item)].append(item) return d
itertools.groupby почти делает то, что вы хотите, за исключением того, что требуется сортировать элементы, чтобы убедиться, что вы получаете один непрерывный диапазон, поэтому вам нужно сначала отсортировать по вашему ключу (в противном случае вы получите несколько чередующихся групп для каждого типа). например.
def is_good(f): return f[2].lower() in IMAGE_TYPES files = [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi'), ('file3.gif', 123L, '.gif')] for key, group in itertools.groupby(sorted(files, key=is_good), key=is_good): print key, list(group)выдает:
False [('file2.avi', 999L, '.avi')] True [('file1.jpg', 33L, '.jpg'), ('file3.gif', 123L, '.gif')]подобно другим решениям, ключевая функция может быть определена для разделения на любое количество групп, которые вы хотите.
лично мне нравится версия, которую вы процитировали, предполагая, что у вас уже есть список
goodvalsошивается. Если нет, то что-то вроде:good = filter(lambda x: is_good(x), mylist) bad = filter(lambda x: not is_good(x), mylist)конечно, это действительно очень похоже на использование понимания списка, как вы изначально делали, но с функцией вместо поиска:
good = [x for x in mylist if is_good(x)] bad = [x for x in mylist if not is_good(x)]в целом, я нахожу эстетику понимания списка очень приятной. Конечно, если вам на самом деле не нужно сохранять порядок и не нужны дубликаты, используя элемент
intersectionиdifferenceметоды на множествах тоже будут хорошо работать.
Если вы хотите сделать это в стиле FP:
good, bad = [ sum(x, []) for x in zip(*(([y], []) if y in goodvals else ([], [y]) for y in mylist)) ]Не самое читаемое решение, но по крайней мере повторяется через mylist только один раз.
def partition(pred, iterable): 'Use a predicate to partition entries into false entries and true entries' # partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9 t1, t2 = tee(iterable) return filterfalse(pred, t1), filter(pred, t2)Регистрация этой
Я думаю, что обобщение разбиения a на итерацию на основе N условий удобно
from collections import OrderedDict def partition(iterable,*conditions): '''Returns a list with the elements that satisfy each of condition. Conditions are assumed to be exclusive''' d= OrderedDict((i,list())for i in range(len(conditions))) for e in iterable: for i,condition in enumerate(conditions): if condition(e): d[i].append(e) break return d.values()например:
ints,floats,other = partition([2, 3.14, 1, 1.69, [], None], lambda x: isinstance(x, int), lambda x: isinstance(x, float), lambda x: True) print " ints: {}\n floats:{}\n other:{}".format(ints,floats,other) ints: [2, 1] floats:[3.14, 1.69] other:[[], None]Если элемент может удовлетворять нескольким условиям, удалить разрыв.
иногда кажется, что понимание списка-это не лучшая вещь для использования !
Я сделал небольшой тест, основанный на ответе людей на эту тему, проверенный на случайном сгенерированном списке. Вот генерация списка (вероятно, есть лучший способ сделать, но это не главное):
good_list = ('.jpg','.jpeg','.gif','.bmp','.png') import random import string my_origin_list = [] for i in xrange(10000): fname = ''.join(random.choice(string.lowercase) for i in range(random.randrange(10))) if random.getrandbits(1): fext = random.choice(good_list) else: fext = "." + ''.join(random.choice(string.lowercase) for i in range(3)) my_origin_list.append((fname + fext, random.randrange(1000), fext))и поехали
# Parand def f1(): return [e for e in my_origin_list if e[2] in good_list], [e for e in my_origin_list if not e[2] in good_list] # dbr def f2(): a, b = list(), list() for e in my_origin_list: if e[2] in good_list: a.append(e) else: b.append(e) return a, b # John La Rooy def f3(): a, b = list(), list() for e in my_origin_list: (b, a)[e[2] in good_list].append(e) return a, b # Ants Aasma def f4(): l1, l2 = tee((e[2] in good_list, e) for e in my_origin_list) return [i for p, i in l1 if p], [i for p, i in l2 if not p] # My personal way to do def f5(): a, b = zip(*[(e, None) if e[2] in good_list else (None, e) for e in my_origin_list]) return list(filter(None, a)), list(filter(None, b)) # BJ Homer def f6(): return filter(lambda e: e[2] in good_list, my_origin_list), filter(lambda e: not e[2] in good_list, my_origin_list)С помощью cmpthese функция, лучший результат-это ответ dbr:
f1 204/s -- -5% -14% -15% -20% -26% f6 215/s 6% -- -9% -11% -16% -22% f3 237/s 16% 10% -- -2% -7% -14% f4 240/s 18% 12% 2% -- -6% -13% f5 255/s 25% 18% 8% 6% -- -8% f2 277/s 36% 29% 17% 15% 9% --
еще одно решение этой проблемы. Мне нужно решение, как можно быстрее. Это означает только одну итерацию по списку и предпочтительно O (1) для добавления данных в один из результирующих списков. Это очень похоже на решение, предоставленное sastanin, только гораздо короче:
from collections import deque def split(iterable, function): dq_true = deque() dq_false = deque() # deque - the fastest way to consume an iterator and append items deque(( (dq_true if function(item) else dq_false).append(item) for item in iterable ), maxlen=0) return dq_true, dq_falseзатем, вы можете использовать функцию следующим образом:
lower, higher = split([0,1,2,3,4,5,6,7,8,9], lambda x: x < 5) selected, other = split([0,1,2,3,4,5,6,7,8,9], lambda x: x in {0,4,9})если вы не в порядке с результатом
dequeобъект, вы можете легко преобразовать его вlist,set, что угодно (напримерlist(lower)). Преобразование происходит намного быстрее, чем непосредственно построение списков.этот метод сохраняет порядок элементов, а также каких-либо дубликатов.
для производительности, попробуйте
itertools.на itertools модуль стандартизирует основной набор быстрых, эффективных инструментов памяти, которые полезны сами по себе или в сочетании. Вместе, они образуют "итератор алгебра", что позволяет сконструировать емко и эффективно специализированные инструменты в чистом Python.
посмотреть itertools.фильтра или imap.
itertools.фильтра(сказуемое, повторяемое)
сделайте итератор, который фильтрует элементы из iterable, возвращая только те, для которых предикат истинен
иногда вам не понадобится эта другая половина списка. Например:
import sys from itertools import ifilter trustedPeople = sys.argv[1].split(',') newName = sys.argv[2] myFriends = ifilter(lambda x: x.startswith('Shi'), trustedPeople) print '%s is %smy friend.' % (newName, newName not in myFriends 'not ' or '')
Если вы настаиваете на умном, вы можете взять решение Виндена и просто немного поддельный ум:
def splay(l, f, d=None): d = d or {} for x in l: d.setdefault(f(x), []).append(x) return d
Если вы не хотите использовать две строки кода для операции, семантика которой нужна только после того, как вы просто обернете некоторые из подходов выше (даже свои собственные) в одну функцию:
def part_with_predicate(l, pred): return [i for i in l if pred(i)], [i for i in l if not pred(i)]это не ленивый подход, и он повторяется дважды по списку, но он позволяет разбить список на одну строку кода.
вдохновленный @gnibbler это большой (но краткого!) ответ, мы можем применить этот подход для отображения на несколько разделов:
from collections import defaultdict def splitter(l, mapper): """Split an iterable into multiple partitions generated by a callable mapper.""" results = defaultdict(list) for x in l: results[mapper(x)].append(x) return resultsзатем
splitterзатем можно использовать следующим образом:>>> l = [1, 2, 3, 4, 2, 3, 4, 5, 6, 4, 3, 2, 3] >>> split = splitter(l, lambda x: x % 2 == 0) # partition l into odds and evens >>> split.items() >>> [(False, [1, 3, 3, 5, 3, 3]), (True, [2, 4, 2, 4, 6, 4, 2])]это работает для более чем двух разделов с более сложным отображением (и на итераторах тоже):
>>> import math >>> l = xrange(1, 23) >>> split = splitter(l, lambda x: int(math.log10(x) * 5)) >>> split.items() [(0, [1]), (1, [2]), (2, [3]), (3, [4, 5, 6]), (4, [7, 8, 9]), (5, [10, 11, 12, 13, 14, 15]), (6, [16, 17, 18, 19, 20, 21, 22])]или с помощью словаря для отображения:
>>> map = {'A': 1, 'X': 2, 'B': 3, 'Y': 1, 'C': 2, 'Z': 3} >>> l = ['A', 'B', 'C', 'C', 'X', 'Y', 'Z', 'A', 'Z'] >>> split = splitter(l, map.get) >>> split.items() (1, ['A', 'Y', 'A']), (2, ['C', 'C', 'X']), (3, ['B', 'Z', 'Z'])]
уже довольно много решений, но еще один способ сделать это было бы -
anims = [] images = [f for f in files if (lambda t: True if f[2].lower() in IMAGE_TYPES else anims.append(t) and False)(f)]перебирает список только один раз, и выглядит немного более подходящие для Python и, следовательно, читаемым для меня.
>>> files = [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi'), ('file1.bmp', 33L, '.bmp')] >>> IMAGE_TYPES = ('.jpg','.jpeg','.gif','.bmp','.png') >>> anims = [] >>> images = [f for f in files if (lambda t: True if f[2].lower() in IMAGE_TYPES else anims.append(t) and False)(f)] >>> print '\n'.join([str(anims), str(images)]) [('file2.avi', 999L, '.avi')] [('file1.jpg', 33L, '.jpg'), ('file1.bmp', 33L, '.bmp')] >>>
Я бы взял двухпроходный подход, отделяя оценку предиката от фильтрации списка:
def partition(pred, iterable): xs = list(zip(map(pred, iterable), iterable)) return [x[1] for x in xs if x[0]], [x[1] for x in xs if not x[0]]Что хорошего в этом, с точки зрения производительности (в дополнение к оценке
predтолько один раз на каждого членаiterable), заключается в том, что он перемещает много логики из интерпретатора и в высокооптимизированный итерационный и картографический код. Это может ускорить итерацию по длинным итерациям, как описано в ответ.экспрессивность-мудрый, он принимает преимущество экспрессивных идиом, таких как понимание и отображение.
решение
from itertools import tee def unpack_args(fn): return lambda t: fn(*t) def separate(fn, lx): return map( unpack_args( lambda i, ly: filter( lambda el: bool(i) == fn(el), ly)), enumerate(tee(lx, 2)))тест
[even, odd] = separate( lambda x: bool(x % 2), [1, 2, 3, 4, 5]) print(list(even) == [2, 4]) print(list(odd) == [1, 3, 5])
мой любимый рецепт это:
goodvals = set(goodvals) good, bad = [], [] _ = [good.append(x) if x in goodvals else bad.append(x) for x in mylist]простой, быстрый и читаемый; так должен был быть Python.
- делая
goodvalsнаset(который использует хэш-таблицу) вместо atuple, мы получаем супер быстрый поиск.- каждый элемент
mylistпроверяется только однажды. это помогает сделать его быстрее._ =Это Питонический способ объявить, что мы специально отбрасываем результат понимания списка. Это не жук.(на основе комментария дансалмо к этой ответ, потому что он, кажется, заслуживает того, чтобы быть своим собственным ответом.)
если вы не возражаете использовать внешнюю библиотеку там два я знаю, что nativly реализовать эту операцию:
>>> files = [ ('file1.jpg', 33, '.jpg'), ('file2.avi', 999, '.avi')] >>> IMAGE_TYPES = ('.jpg','.jpeg','.gif','.bmp','.png')
iteration_utilities.partition:>>> from iteration_utilities import partition >>> notimages, images = partition(files, lambda x: x[2].lower() in IMAGE_TYPES) >>> notimages [('file2.avi', 999, '.avi')] >>> images [('file1.jpg', 33, '.jpg')]>>> from more_itertools import partition >>> notimages, images = partition(lambda x: x[2].lower() in IMAGE_TYPES, files) >>> list(notimages) # returns a generator so you need to explicitly convert to list. [('file2.avi', 999, '.avi')] >>> list(images) [('file1.jpg', 33, '.jpg')]
Не уверен, что это хороший подход, но это можно сделать и таким образом
IMAGE_TYPES = ('.jpg','.jpeg','.gif','.bmp','.png') files = [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi')] images, anims = reduce(lambda (i, a), f: (i + [f], a) if f[2] in IMAGE_TYPES else (i, a + [f]), files, ([], []))
например, разбиение списка на четные и нечетные
arr = range(20) even, odd = reduce(lambda res, next: res[next % 2].append(next) or res, arr, ([], []))или:
def split(predicate, iterable): return reduce(lambda res, e: res[predicate(e)].append(e) or res, iterable, ([], []))плюсы:
- кратчайший возможный путь
- предикат применяется только один раз для каждого элемента
недостатки
- требуется знание парадигмы функционального программирования
def partition(pred, seq): return reduce( lambda (yes, no), x: (yes+[x], no) if pred(x) else (yes, no+[x]), seq, ([], []) )
Comments