首页 文章

Python:根据条件拆分列表?

提问于
浏览
210

从美学角度和绩效角度来看,根据条件将项目列表拆分为多个列表的最佳方法是什么?相当于:

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 回答

  • 0

    good = [x表示m中的x,如果x表示良品]
    bad = [如果x不在商品中,则x为x in mylist]
    有没有更优雅的方式来做到这一点?

    该代码完全可读,非常清晰!

    # 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
    
  • 0
    good, bad = [], []
    for x in mylist:
        (bad, good)[x in goodvals].append(x)
    
  • 5

    这是懒惰的迭代器方法:

    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

    所有提出的解决方案的问题在于它将扫描并应用过滤功能两次 . 我会做一个像这样的简单小函数:

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

    这样你就不会处理任何两次而且也不会重复代码 .

  • 3

    我接受它 . 我提出了一个惰性的单遍 partition 函数,它保留了输出子序列中的相对顺序 .

    1.要求

    我认为要求是:

    • 维护元素的相对顺序(因此,没有集合和字典)

    • 仅为每个元素评估条件一次(因此不使用( ifiltergroupby

    • 允许任意序列的延迟消耗(如果我们能够预先计算它们,那么天真的实现也可能是可接受的)

    2.拆分库

    我的 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.解析分区功能

    在内部,我们需要同时构建两个子序列,因此仅消耗一个输出序列将强制另一个也被计算 . 我们需要在用户请求(存储已处理但尚未请求的元素)之间保持状态 . 为了保持状态,我使用两个双端队列( 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 函数 . 它需要一个条件函数和一个序列(就像 mapfilter ),并返回两个生成器 . 第一个生成器构建条件成立的元素的子序列,第二个构建补充子序列 . 迭代器和生成器允许甚至长或无限序列的惰性分裂 .

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

    我选择测试函数作为第一个促进将来部分应用的参数(类似于 mapfilter 如何将测试函数作为第一个参数) .

  • 2

    First go (OP前编辑):使用集合:

    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)和性能都有好处 .

    Second go (后OP编辑):

    创建一个好的扩展列表作为一组:

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

    这将提高性能 . 否则,你看到的对我来说很好 .

  • 0

    我基本上喜欢安德斯的方法,因为它很一般 . 这是一个将分类程序放在第一位(匹配过滤器语法)并使用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
    
  • 163

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

    与其他解决方案类似,可以将键功能定义为分成您想要的任意数量的组 .

  • 1

    就个人而言,我喜欢你引用的版本,假设你已经有一个 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)]
    

    总的来说,我发现列表理解的美学非常令人愉悦 . 当然,如果你不需要重复,那么在集合上使用 intersectiondifference 方法也会很有效 .

  • 13

    如果你想在FP风格:

    good, bad = [ sum(x, []) for x in zip(*(([y], []) if y in goodvals else ([], [y])
                                            for y in mylist)) ]
    

    不是最易读的解决方案,但至少只通过mylist迭代一次 .

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

    检查this

  • -1

    我认为基于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]
    

    如果元素可能满足多个条件,则删除中断 .

  • 2

    有时,看起来列表理解不是最好用的!

    我根据人们对这个主题给出的答案进行了一点测试,并在随机生成的列表中进行了测试 . 这是列表的生成(可能有更好的方法,但不是重点):

    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%  --
    
  • 0

    这个问题的又一个解决方案 . 我需要一个尽可能快的解决方案 . 这意味着列表上只有一次迭代,最好是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 对象不满意,您可以轻松地将其转换为 listset ,无论您喜欢什么(例如 list(lower) ) . 转换速度要快得多,直接构建列表 .

    此方法保持项目的顺序以及任何重复项 .

  • 4

    要获得性能,请尝试 itertools .

    itertools模块标准化了一组快速,内存有效的工具,这些工具本身或组合使用 . 它们共同组成了一个“迭代器代数”,可以在纯Python中简洁有效地构建专用工具 .

    itertools.ifilter或imap .

    itertools.ifilter(predicate,iterable)创建一个迭代器,用于过滤来自iterable的元素,仅返回谓词为True的元素

  • 0

    有时您不需要列表的另一半 . 例如:

    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 '')
    
  • 18

    受@ gnibbler的great (but terse!) answer的启发,我们可以应用该方法映射到多个分区:

    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)] += [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'])]
    
  • 13

    如果你坚持聪明,你可以采取Winden的解决方案,只是有点虚假的聪明:

    def splay(l, f, d=None):
      d = d or {}
      for x in l: d.setdefault(f(x), []).append(x)
      return d
    
  • 1

    如果您只关心上面的一些方法(甚至是您自己的方法)在单个函数中使用,那么您的关注点不是为一个只需要语义的操作使用两行代码:

    def part_with_predicate(l, pred):
        return [i for i in l if pred(i)], [i for i in l if not pred(i)]
    

    它不是一个惰性eval方法,它在列表中迭代两次,但它允许您在一行代码中对列表进行分区 .

  • 22

    这里已经有很多解决方案,但另一种方法是 -

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

    只在列表上迭代一次,看起来更加pythonic,因此对我来说可读 .

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

    我采用2遍方法,将谓词的评估与过滤列表分开:

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

    这对性能有什么好处(除了在 iterable 的每个成员上只评估一次 pred 之外),它将大量逻辑移出解释器并进入高度优化的迭代和映射代码 . 这可以加速长迭代的迭代,如in this answer所述 .

    表达性方面,它利用了理解和映射等富有表现力的习语 .

  • 0

    解决方案

    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])
    
  • 1

    我最喜欢的食谱是:

    goodvals = set(goodvals)    
    good, bad = [], []
    _ = [good.append(x) if x in goodvals else bad.append(x) for x in mylist]
    

    简单,快速,可读; Python本来就是这样的 .

    • 通过将 goodvals 变为 set (使用哈希表)而不是 tuple ,我们获得超快速查找 .

    • mylist 中的每个项目仅检查一次 . 这有助于加快速度 .

    • _ = 是一种Pythonic方式,用于声明我们有意丢弃列表理解的结果 . 这不是一个错误 .

    (基于dansalmo对_735878的回答,因为它似乎应该是它自己的答案 . )

  • 0

    如果您不介意使用外部库,我知道有两个本地实现此操作:

    >>> files = [ ('file1.jpg', 33, '.jpg'), ('file2.avi', 999, '.avi')]
    >>> IMAGE_TYPES = ('.jpg','.jpeg','.gif','.bmp','.png')
    
    >>> 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')]
    
  • 89

    不确定这是否是一个好方法,但也可以这样做

    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, ([], []))
    
  • 9

    例如,按偶数和奇数拆分列表

    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, ([], []))
    

    Advantages:

    • 最短的无形方式

    • 谓词仅对每个元素应用一次

    Disadvantages

    • 需要功能知识编程范式
  • 0
    def partition(pred, seq):
      return reduce( lambda (yes, no), x: (yes+[x], no) if pred(x) else (yes, no+[x]), seq, ([], []) )
    

相关问题