Python: разделить список на основе условия? - PullRequest
227 голосов
/ 04 июня 2009

Как лучше, как с эстетической точки зрения, так и с точки зрения производительности, разделить список элементов на несколько списков на основе условного обозначения? Эквивалент:

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]

Ответы [ 29 ]

175 голосов
/ 27 августа 2012
good, bad = [], []
for x in mylist:
    (bad, good)[x in goodvals].append(x)
98 голосов
/ 04 июня 2009

Вот подход ленивого итератора:

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, b

Редактировать: Для более конкретного случая разделения элементов по разным спискам по некоторому ключу здесь есть общая функция, которая делает это:

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']
94 голосов
/ 04 июня 2009
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]

Опять же, это отлично!

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

На самом деле, я могу сделать еще один шаг "назад" и просто использовать простой цикл 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
24 голосов
/ 04 июня 2009

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

def SplitIntoTwoLists(l, f):
  a = []
  b = []
  for i in l:
    if f(i):
      a.append(i)
    else:
      b.append(i)
 return (a,b)

Таким образом, вы ничего не обрабатываете дважды, а также не повторяете код.

19 голосов
/ 24 октября 2011

Мой взгляд на это. Я предлагаю ленивую однопроходную функцию partition, который сохраняет относительный порядок в выходных подпоследовательностях.

1. Требования

Я предполагаю, что требования:

  • поддерживать относительный порядок элементов (следовательно, нет наборов и словари)
  • оценивать условие только один раз для каждого элемента (следовательно, не используя (i) filter или groupby)
  • допускает ленивое потребление любой последовательности (если мы можем позволить себе предварительно вычислить их, то наивная реализация, вероятно, будет тоже приемлемо)

2. split библиотека

Моя partition функция (представленная ниже) и другие подобные функции превратили его в небольшую библиотеку:

Обычно устанавливается через PyPI:

pip install --user split

Чтобы разделить список по условию, используйте функцию partition:

>>> from split import partition
>>> files = [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi') ]
>>> image_types = ('.jpg','.jpeg','.gif','.bmp','.png')
>>> images, other = partition(lambda f: f[-1] in image_types, files)
>>> list(images)
[('file1.jpg', 33L, '.jpg')]
>>> list(other)
[('file2.avi', 999L, '.avi')]

3. partition объясненная функция

Внутренне нам нужно построить две подпоследовательности одновременно, так что только одна выходная последовательность приведет к вычислению другой тоже. И нам нужно сохранять состояние между пользовательскими запросами (хранилище обработано но еще не запрошенные элементы). Чтобы сохранить состояние, я использую два двухсторонних очереди (deques):

from collections import deque

SplitSeq класс заботится о ведении домашнего хозяйства:

class SplitSeq:
    def __init__(self, condition, sequence):
        self.cond = condition
        self.goods = deque([])
        self.bads = deque([])
        self.seq = iter(sequence)

Магия происходит по методу .getNext(). Это почти как .next() итераторов, но позволяет указать, какой тип элемента мы хотим этот раз. За сценой не отбрасываются отклоненные элементы, но вместо этого помещает их в одну из двух очередей:

    def getNext(self, getGood=True):
        if getGood:
            these, those, cond = self.goods, self.bads, self.cond
        else:
            these, those, cond = self.bads, self.goods, lambda x: not self.cond(x)
        if these:
            return these.popleft()
        else:
            while 1: # exit on StopIteration
                n = self.seq.next()
                if cond(n):
                    return n
                else:
                    those.append(n)

Конечный пользователь должен использовать функцию partition. Требуется функция условия и последовательность (как map или filter), и возвращает два генератора. Первый генератор строит подпоследовательность элементы, для которых выполняется условие, второй строит дополнительная подпоследовательность. Итераторы и генераторы допускают ленивость расщепление даже длинных или бесконечных последовательностей.

def partition(condition, sequence):
    cond = condition if condition else bool  # evaluate as bool if condition == None
    ss = SplitSeq(cond, sequence)
    def goods():
        while 1:
            yield ss.getNext(getGood=True)
    def bads():
        while 1:
            yield ss.getNext(getGood=False)
    return goods(), bads()

Я выбрал тестовую функцию в качестве первого аргумента для облегчения частичное применение в будущем (аналогично тому, как map и filter иметь тестовую функцию в качестве первого аргумента).

13 голосов
/ 19 июля 2010

Мне в основном нравится подход Андерса, так как он очень общий. Вот версия, которая ставит классификатор на первое место (для соответствия синтаксису фильтра) и использует 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
13 голосов
/ 04 июня 2009

First go (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]

Это хорошо как для удобочитаемости, так и для производительности.

Второй ход (после редактирования OP):

Создайте список хороших расширений в виде набора:

IMAGE_TYPES = set(['.jpg','.jpeg','.gif','.bmp','.png'])

и это повысит производительность. В противном случае, то, что у вас есть, выглядит хорошо для меня.

9 голосов
/ 04 июня 2009

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

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

5 голосов
/ 04 июня 2009

Лично мне нравится версия, которую вы цитировали, при условии, что у вас уже есть список 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 на наборах также будет хорошо работать.

4 голосов
/ 01 мая 2015

Я думаю, что обобщение разбиения итерируемого на основе 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]

Если элемент может удовлетворять нескольким условиям, удалите разрыв.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...