Python: diviser une liste en fonction d'une condition?
Quelle est la meilleure façon, à la fois esthétiquement et du point de vue de la performance, de diviser une liste d'éléments en plusieurs listes basées sur un conditionnel? L'équivalent de:
good = [x for x in mylist if x in goodvals]
bad = [x for x in mylist if x not in goodvals]
Y a-t-il une façon plus élégante de le faire?
Update: 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]
27 réponses
good = [x for x in mylist if x in goodvals] bad = [x for x in mylist if x not in goodvals]
Y a-t-il une façon plus élégante de le faire?
Ce code est parfaitement lisible, et extrêmement clair!
# 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]
Encore une fois, c'est très bien!
Il peut y avoir de légères améliorations de performances en utilisant des ensembles, mais c'est une différence triviale, et je trouve la compréhension de la liste beaucoup plus facile à lire, et vous n'avez pas à vous soucier de l'ordre étant foiré, les doublons étant supprimés comme ainsi de suite.
En fait, je peux faire un autre pas "en arrière", et utilisez simplement une boucle for simple:
images, anims = [], []
for f in files:
if f.lower() in IMAGE_TYPES:
images.append(f)
else:
anims.append(f)
La compréhension d'une liste ou l'utilisation de set()
est correcte jusqu'à ce que vous ayez besoin d'ajouter une autre vérification ou un autre peu de logique - disons que vous voulez supprimer tous les jpeg de 0 octet, vous ajoutez juste quelque chose comme..
if f[1] == 0:
continue
good, bad = [], []
for x in mylist:
(bad, good)[x in goodvals].append(x)
Voici l'approche paresseuse de l'itérateur:
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)
Il évalue la condition une fois par élément et renvoie deux générateurs, d'abord en donnant des valeurs de la séquence où la condition est vraie, l'autre où elle est fausse.
Parce que c'est paresseux, vous pouvez l'utiliser sur n'importe quel itérateur, même infini:
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)))
Habituellement, bien que l'approche de retour de liste non-paresseuse soit meilleure:
def split_on_condition(seq, condition):
a, b = [], []
for item in seq:
(a if condition(item) else b).append(item)
return a, b
Edit: pour votre cas d'utilisation plus spécifique de diviser des éléments en différentes listes par une clé, voici une fonction générique qui fait cela:
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
Utilisation:
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']
Le problème avec toutes les solutions proposées est qu'il va scanner et appliquer la fonction de filtrage deux fois. Je ferais une petite fonction simple comme ceci:
def SplitIntoTwoLists(l, f):
a = []
b = []
for i in l:
if f(i):
a.append(i)
else:
b.append(i)
return (a,b)
De cette façon, vous ne traitez rien deux fois et ne répétez pas non plus le code.
Mon point de vue. Je propose une fonction paresseuse, en un seul passage, partition
,
qui préserve l'ordre relatif dans les sous-séquences de sortie.
1. Exigences
Je suppose que les exigences sont:
- maintient l'ordre relatif des éléments (par conséquent, aucun les dictionnaires)
- évaluer la condition une seule fois pour chaque élément (donc ne pas utiliser
(
i
)filter
ougroupby
) - permettre une consommation paresseuse de l'une ou l'autre séquence (si nous pouvons nous le permettre précalculer, l' naïf mise en œuvre est susceptible d'être acceptable aussi)
2. split
bibliothèque
Ma fonction partition
(présentée ci-dessous) et d'autres fonctions similaires
l'ont fait dans une petite bibliothèque:
Il est installable normalement via PyPI:
pip install --user split
Pour diviser une base de liste sur condition, utilisez la fonction 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
fonction expliquée
En interne, nous devons construire deux sous-séquences à la fois, donc consommer
une seule séquence de sortie forcera l'autre à être calculée
trop. Et nous devons garder l'état entre les demandes des utilisateurs (magasin traité
mais pas encore demandé d'éléments). Pour garder l'état, j'utilise deux doubles extrémités
les files d'attente (deques
):
from collections import deque
SplitSeq
la classe s'occupe du ménage:
class SplitSeq:
def __init__(self, condition, sequence):
self.cond = condition
self.goods = deque([])
self.bads = deque([])
self.seq = iter(sequence)
La magie se produit dans sa méthode .getNext()
. C'est presque comme .next()
des itérateurs, mais permet de spécifier quel type d'élément nous voulons
cette fois. Derrière la scène il ne jette pas le rejeté des éléments de,
mais à la place les met 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 la fonction partition
. Il faut un
fonction de condition et une séquence (tout comme map
ou filter
), et
retourne deux générateurs. Le premier générateur construit une sous-séquence de
éléments pour lesquels la condition est valable, le second construit le
sous-séquence complémentaire. Itérateurs et générateurs permettent pour paresseux
fractionnement 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 le fonction de test pour être le premier argument pour faciliter
application partielle à l'avenir (similaire à comment map
et filter
avoir la fonction de test comme premier argument).
Tout d'abord aller (pre-OP-edit): utiliser les 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 bon pour la lisibilité (à mon humble avis) et la performance.
Deuxième aller (post-OP-edit):
Créez votre liste de bonnes extensions en tant qu'ensemble:
IMAGE_TYPES = set(['.jpg','.jpeg','.gif','.bmp','.png'])
Et cela augmentera les performances. Sinon, ce que vous avez me semble bien.
J'aime fondamentalement l'approche D'Anders car elle est très générale. Voici une version qui met le catégoriseur en premier (pour correspondre à la syntaxe du filtre) et utilise un defaultdict (supposé importé).
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 fait presque ce que vous voulez, sauf qu'il nécessite que les éléments soient triés pour vous 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). par exemple.
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')]
Semblable aux autres solutions, la fonction clé peut être définie pour diviser en un nombre quelconque de groupes que vous voulez.
Personnellement, j'aime la version que vous avez Citée, en supposant que vous avez déjà une liste de goodvals
traîner. Si non, quelque chose comme:
good = filter(lambda x: is_good(x), mylist)
bad = filter(lambda x: not is_good(x), mylist)
Bien sûr, c'est vraiment très similaire à l'utilisation d'une compréhension de liste comme vous l'a fait à l'origine, mais avec une fonction au lieu d'une recherche:
good = [x for x in mylist if is_good(x)]
bad = [x for x in mylist if not is_good(x)]
En général, je trouve l'esthétique des compréhensions de liste très agréable. Bien sûr, si vous n'avez pas réellement besoin de conserver la commande et n'avez pas besoin de doublons, utilisez les intersection
et difference
les méthodes sur les ensembles fonctionneraient bien aussi.
Si vous voulez le faire dans le style FP:
good, bad = [ sum(x, []) for x in zip(*(([y], []) if y in goodvals else ([], [y])
for y in mylist)) ]
Pas la solution la plus lisible, mais au moins itère à travers mylist une seule fois.
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)
Vérifier ce
Je pense qu'une généralisation de la division d'un itérable basé sur N conditions est pratique
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()
Par exemple:
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]
Si l'élément peut satisfaire à plusieurs conditions, supprimer la rupture.
Parfois, il semble que la compréhension de liste n'est pas la meilleure chose à utiliser !
J'ai fait un petit test basé sur la réponse que les gens ont donnée à ce sujet, testé sur une liste générée au hasard. Voici la génération de la liste (il y a probablement une meilleure façon de le faire, mais ce n'est pas le point):
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))
Et voilà
# 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)
En utilisant la fonction cmpthese, le meilleur résultat est la réponse 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% --
Encore une autre solution à ce problème. J'ai besoin d'une solution qui est aussi rapide que possible. Cela signifie une seule itération sur la liste et de préférence O (1) pour ajouter des données à l'une des listes résultantes. Ceci est très similaire à la solution fournie par sastanin, sauf beaucoup plus courte:
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
Ensuite, vous pouvez utiliser la fonction de la manière suivante:
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})
Si vous n'êtes pas d'accord avec l'objet deque
résultant, vous pouvez facilement le convertir en list
, set
, comme tu veux (par exemple list(lower)
). La conversion est beaucoup plus rapide, que la construction des listes directement.
Cette méthode conserve l'ordre des éléments, ainsi que tous les doublons.
Pour la performance, essayez itertools
.
Le module itertools standardise un ensemble de base d'outils rapides et efficaces en mémoire qui sont utiles seuls ou en combinaison. Ensemble, ils forment une "algèbre d'itérateur" permettant de construire des outils spécialisés de manière succincte et efficace en Python pur.
Voir itertools.ifilter {[6] } ou imap.
Itertools.ifilter (prédicat, itérable)
Créez un itérateur qui filtre les éléments de iterable ne renvoyant que ceux pour lesquels le prédicat est vrai
Parfois, vous n'aurez pas besoin de cette autre moitié de la liste. Par exemple:
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 '')
Si vous insistez sur clever, vous pourriez prendre la solution de Winden et juste un peu fausse intelligence:
def splay(l, f, d=None):
d = d or {}
for x in l: d.setdefault(f(x), []).append(x)
return d
Si votre préoccupation est de ne pas utiliser deux lignes de code pour une opération dont la sémantique n'a besoin qu'une fois que vous enveloppez certaines des approches ci-dessus (même les vôtres) dans une seule fonction:
def part_with_predicate(l, pred):
return [i for i in l if pred(i)], [i for i in l if not pred(i)]
Ce n'est pas une approche paresseuse et elle parcourt deux fois la liste, mais elle vous permet de partitionner la liste en une seule ligne de code.
Inspiré par @gnibbler's génial (mais laconique!) réponse , Nous pouvons appliquer cette approche à la carte à plusieurs partitions:
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
Alors splitter
peut alors être utilisé comme suit:
>>> 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])]
Cela fonctionne pour plus de deux partitions avec un mappage plus compliqué (et sur les itérateurs aussi):
>>> 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])]
Ou utiliser un dictionnaire pour mapper:
>>> 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'])]
Déjà quelques solutions ici, mais encore une autre façon de le faire serait -
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)]
Itère sur la liste une seule fois, et me semble un peu plus pythonique et donc lisible.
>>> 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')]
>>>
Je prendrais une approche à 2 passes, séparant l'évaluation du prédicat du filtrage de la liste:
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]]
Ce qui est bien à ce sujet, en termes de performances (en plus d'évaluer pred
une seule fois sur chaque membre de iterable
), c'est qu'il déplace beaucoup de logique hors de l'interpréteur et dans un code d'itération et de mappage hautement optimisé. Cela peut accélérer l'itération sur de longues itérables, comme décrit dans cette réponse .
Expressivité-sage, Il tire parti des idiomes expressifs comme compréhensions et cartographie.
Solution
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)))
Essai
[even, odd] = separate(
lambda x: bool(x % 2),
[1, 2, 3, 4, 5])
print(list(even) == [2, 4])
print(list(odd) == [1, 3, 5])
Ma recette préférée pour cela est:
goodvals = set(goodvals)
good, bad = [], []
_ = [good.append(x) if x in goodvals else bad.append(x) for x in mylist]
Simple, rapide et lisible; la façon dont Python était censé être.
- En faisant
goodvals
enset
(qui utilise une table de hachage) au lieu d'untuple
, nous obtenons des recherches super rapides. - Chaque élément de
mylist
est vérifiée uniquement lorsque. cela aide à le rendre plus rapide. -
_ =
est un moyen pythonique de déclarer que nous rejetons le résultat de la compréhension de la liste exprès. Ce n'est pas un bug.
(basé sur dansalmo commentez à cette réponse , car elle semble mériter d'être sa propre réponse.)
Si cela ne vous dérange pas d'utiliser une bibliothèque externe là-bas deux je sais que nativly implémenter cette opération:
>>> 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')]
Ne sais Pas si c'est une bonne approche, mais il peut être fait de cette façon
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, ([], []))
Par exemple, diviser la liste par Pair et Impair
arr = range(20)
even, odd = reduce(lambda res, next: res[next % 2].append(next) or res, arr, ([], []))
Ou en général:
def split(predicate, iterable):
return reduce(lambda res, e: res[predicate(e)].append(e) or res, iterable, ([], []))
Avantages:
- la plus courte possible, de manière
- le prédicat s'applique une seule fois pour chaque élément
Inconvénients
- nécessite la connaissance du paradigme de programmation fonctionnelle
def partition(pred, seq):
return reduce( lambda (yes, no), x: (yes+[x], no) if pred(x) else (yes, no+[x]), seq, ([], []) )