395 votes

Comment diviser une liste en fonction d'une condition ?

Quelle est la meilleure façon, à la fois esthétiquement et du point de vue des performances, de diviser une liste d'éléments en plusieurs listes sur la base d'une condition ? L'équivalent de :

good = [x for x in mylist if x in goodvals]
bad  = [x for x in mylist if x not in goodvals]

Existe-t-il une manière plus élégante de procéder ?

Voici le cas d'utilisation réel, pour mieux expliquer ce que j'essaie de faire :

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

9 votes

J'ai atterri ici à la recherche d'un moyen d'avoir une condition dans l'instruction set builder, votre question a répondu à la mienne :)

8 votes

diviser est une description malheureuse de cette opération, puisqu'elle a déjà une signification spécifique en ce qui concerne les chaînes Python. Je pense que diviser est un terme plus précis (ou du moins moins surchargé dans le contexte des itérables Python) pour décrire cette opération. J'ai atterri ici à la recherche d'un équivalent de liste de str.split() , à diviser la liste en une collection ordonnée de sous-listes consécutives. Par exemple split([1,2,3,4,5,3,6], 3) -> ([1,2],[4,5],[6]) frente a diviser les éléments d'une liste par catégorie.

0 votes

Discussion du même sujet sur python-list.

18voto

sastanin Points 16061

Mon avis sur la question. Je propose un passage unique et paresseux, partition fonction, qui préserve l'ordre relatif dans les sous-séquences de sortie.

1. Exigences

Je suppose que les exigences sont les suivantes :

  • maintenir l'ordre relatif des éléments (donc pas d'ensembles et de dictionnaires)
  • n'évalue la condition qu'une seule fois pour chaque élément (c'est pourquoi il n'est pas nécessaire d'utiliser ( i ) filter o groupby )
  • permettent une consommation paresseuse de l'une ou l'autre des séquences (si nous pouvons nous permettre de les précalculer). les précalculer, alors la mise en œuvre naïve sera probablement acceptable)

2. split bibliothèque

Mon partition (présentée ci-dessous) et d'autres fonctions similaires ont été intégrées dans une petite bibliothèque :

Il est installable normalement via PyPI :

pip install --user split

Pour diviser une liste en fonction d'une condition, utilisez partition fonction :

>>> 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 la fonction expliquée

En interne, nous devons construire deux sous-séquences à la fois. une seule séquence de sortie forcera l'autre à être calculée également. aussi. Et nous devons conserver l'état entre les requêtes des utilisateurs (stocker les données traitées mais pas encore demandés). Pour conserver l'état, j'utilise deux files d'attente à double extrémité à double extrémité ( deques ):

from collections import deque

SplitSeq La classe s'occupe de l'entretien ménager :

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

La magie opère dans son .getNext() méthode. C'est presque comme .next() des itérateurs, mais permet de spécifier le type d'élément que nous voulons cette fois-ci. cette fois. Derrière la scène, il ne rejette pas les éléments rejetés, mais les place dans l'une des deux files d'attente : la file d'attente des éléments rejetés, mais les place dans l'une des deux files d'attente :

    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)

L'utilisateur final est censé utiliser partition fonction. Elle prend une et une séquence (tout comme map o filter ), et renvoie deux générateurs. Le premier générateur construit une sous-séquence de pour lesquels la condition tient, le second construit la sous-séquence complémentaire. Les itérateurs et les générateurs permettent de diviser par paresseux de séquences même longues ou infinies.

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

J'ai choisi la fonction de test comme premier argument pour faciliter la mise en place de la fonction de test. l'application partielle à l'avenir (de la même manière que map y filter ont pour premier argument la fonction de test).

17voto

ChaimG Points 2634

Elégant et rapide

Inspiré par le commentaire de DanSalmo, voici une solution concise, élégante et en même temps l'une des plus rapides.

good_set = set(goodvals)
good, bad = [], []
for item in my_list:
    good.append(item) if item in good_set else bad.append(item)

Conseil : tourner goodvals dans un ensemble nous permet de gagner facilement en rapidité.

Le plus rapide

Pour une rapidité maximale, nous prenons la réponse la plus rapide et la turbocompressons en transformant good_list en un ensemble. Cette seule opération nous permet d'augmenter la vitesse de plus de 40 %, et nous obtenons une solution plus de 5,5 fois plus rapide que la solution la plus lente, tout en restant lisible.

good_list_set = set(good_list)  # 40%+ faster than a tuple.

good, bad = [], []
for item in my_origin_list:
    if item in good_list_set:
        good.append(item)
    else:
        bad.append(item)

Un peu plus court

Il s'agit d'une version plus concise de la réponse précédente.

good_list_set = set(good_list)  # 40%+ faster than a tuple.

good, bad = [], []
for item in my_origin_list:
    out = good if item in good_list_set else bad
    out.append(item)

L'élégance peut être quelque peu subjective, mais certaines solutions de type Rube Goldberg qui sont mignonnes et ingénieuses sont assez préoccupantes et ne devraient pas être utilisées dans le code de production, quel que soit le langage, et encore moins en python qui est élégant dans l'âme.


Résultats de l'évaluation comparative :

filter_BJHomer                  80/s       --   -3265%   -5312%   -5900%   -6262%   -7273%   -7363%   -8051%   -8162%   -8244%
zip_Funky                       118/s    4848%       --   -3040%   -3913%   -4450%   -5951%   -6085%   -7106%   -7271%   -7393%
two_lst_tuple_JohnLaRoy         170/s   11332%    4367%       --   -1254%   -2026%   -4182%   -4375%   -5842%   -6079%   -6254%
if_else_DBR                     195/s   14392%    6428%    1434%       --    -882%   -3348%   -3568%   -5246%   -5516%   -5717%
two_lst_compr_Parand            213/s   16750%    8016%    2540%     967%       --   -2705%   -2946%   -4786%   -5083%   -5303%
if_else_1_line_DanSalmo         292/s   26668%   14696%    7189%    5033%    3707%       --    -331%   -2853%   -3260%   -3562%
tuple_if_else                   302/s   27923%   15542%    7778%    5548%    4177%     343%       --   -2609%   -3029%   -3341%
set_1_line                      409/s   41308%   24556%   14053%   11035%    9181%    3993%    3529%       --    -569%    -991%
set_shorter                     434/s   44401%   26640%   15503%   12303%   10337%    4836%    4345%     603%       --    -448%
set_if_else                     454/s   46952%   28358%   16699%   13349%   11290%    5532%    5018%    1100%     469%       --

Le code de référence complet pour Python 3.7 (modifié à partir de FunkySayu) :

good_list = ['.jpg','.jpeg','.gif','.bmp','.png']

import random
import string
my_origin_list = []
for i in range(10000):
    fname = ''.join(random.choice(string.ascii_lowercase) for i in range(random.randrange(10)))
    if random.getrandbits(1):
        fext = random.choice(list(good_list))
    else:
        fext = "." + ''.join(random.choice(string.ascii_lowercase) for i in range(3))

    my_origin_list.append((fname + fext, random.randrange(1000), fext))

# Parand
def two_lst_compr_Parand(*_):
    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 if_else_DBR(*_):
    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 two_lst_tuple_JohnLaRoy(*_):
    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 zip_Funky(*_):
    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 filter_BJHomer(*_):
    return list(filter(lambda e: e[2] in good_list, my_origin_list)), list(filter(lambda e: not e[2] in good_list,                                                                             my_origin_list))

# ChaimG's answer; as a list.
def if_else_1_line_DanSalmo(*_):
    good, bad = [], []
    for e in my_origin_list:
        _ = good.append(e) if e[2] in good_list else bad.append(e)
    return good, bad

# ChaimG's answer; as a set.
def set_1_line(*_):
    good_list_set = set(good_list)
    good, bad = [], []
    for e in my_origin_list:
        _ = good.append(e) if e[2] in good_list_set else bad.append(e)
    return good, bad

# ChaimG set and if else list.
def set_shorter(*_):
    good_list_set = set(good_list)
    good, bad = [], []
    for e in my_origin_list:
        out = good if e[2] in good_list_set else bad
        out.append(e)
    return good, bad

# ChaimG's best answer; if else as a set.
def set_if_else(*_):
    good_list_set = set(good_list)
    good, bad = [], []
    for e in my_origin_list:
        if e[2] in good_list_set:
            good.append(e)
        else:
            bad.append(e)
    return good, bad

# ChaimG's best answer; if else as a set.
def tuple_if_else(*_):
    good_list_tuple = tuple(good_list)
    good, bad = [], []
    for e in my_origin_list:
        if e[2] in good_list_tuple:
            good.append(e)
        else:
            bad.append(e)
    return good, bad

def cmpthese(n=0, functions=None):
    results = {}
    for func_name in functions:
        args = ['%s(range(256))' % func_name, 'from __main__ import %s' % func_name]
        t = Timer(*args)
        results[func_name] = 1 / (t.timeit(number=n) / n) # passes/sec

    functions_sorted = sorted(functions, key=results.__getitem__)
    for f in functions_sorted:
        diff = []
        for func in functions_sorted:
            if func == f:
                diff.append("--")
            else:
                diff.append(f"{results[f]/results[func]*100 - 100:5.0%}")
        diffs = " ".join(f'{x:>8s}' for x in diff)

        print(f"{f:27s} \t{results[f]:,.0f}/s {diffs}")

if __name__=='__main__':
    from timeit import Timer
cmpthese(1000, 'two_lst_compr_Parand if_else_DBR two_lst_tuple_JohnLaRoy zip_Funky filter_BJHomer if_else_1_line_DanSalmo set_1_line set_if_else tuple_if_else set_shorter'.split(" "))

15voto

RichieHindle Points 98544

Premier essai (avant l'édition de l'OP) : Utiliser des ensembles :

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]

C'est une bonne chose pour la lisibilité (IMHO) et la performance.

Deuxième tour (post-OP-edit) :

Créez votre liste de bonnes extensions en bloc :

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

et cela augmentera les performances. Sinon, ce que vous avez me semble correct.

13voto

Brian Points 48423

itertools.groupby fait presque ce que vous voulez, sauf qu'il faut trier les éléments pour s'assurer d'obtenir une seule plage contiguë, donc vous devez d'abord trier par votre clé (sinon vous obtiendrez plusieurs groupes entrelacés pour chaque type). ex.

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)

donne :

False [('file2.avi', 999L, '.avi')]
True [('file1.jpg', 33L, '.jpg'), ('file3.gif', 123L, '.gif')]

Comme pour les autres solutions, la fonction clé peut être définie de manière à être divisée en autant de groupes que vous le souhaitez.

12voto

John D Points 2109
good.append(x) if x in goodvals else bad.append(x)

Cette réponse élégante et concise de @dansalmo s'est retrouvée enfouie dans les commentaires, je la republie donc ici en tant que réponse afin qu'elle reçoive l'attention qu'elle mérite, en particulier pour les nouveaux lecteurs.

Exemple complet :

good, bad = [], []
for x in my_list:
    good.append(x) if x in goodvals else bad.append(x)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X