Modo elegante per rimuovere elementi dalla sequenza in Python?


50

Quando scrivo codice in Python, spesso devo rimuovere elementi da un elenco o da un altro tipo di sequenza in base ad alcuni criteri. Non ho trovato una soluzione che sia elegante ed efficiente, in quanto la rimozione di elementi da una lista che stai attualmente iterando è negativa. Ad esempio, non si può fare questo:

for name in names: 
    if name[-5:] == 'Smith': 
     names.remove(name) 

Io di solito finiscono per fare qualcosa di simile:

toremove = [] 
for name in names: 
    if name[-5:] == 'Smith': 
     toremove.append(name) 
for name in toremove: 
    names.remove(name) 
del toremove 

Questo è innefficient, piuttosto brutto e possibilmente buggy (Come maneggia multipla 'John Voci di Smith?). Qualcuno ha una soluzione più elegante, o almeno una più efficiente?

Che ne dici di uno che funziona con i dizionari?

  0

Il codice rimuove più sms o lo hai modificato? 20 lug. 102010-07-20 12:24:30

52

due semplici modi per realizzare proprio il filtraggio sono:

  1. Utilizzando filter:

    names = filter(lambda name: name[-5:] != "Smith", names)

  2. Uso list comprehension:

    names = [name for name in names if name[-5:] != "Smith"]

noti che entrambi i casi mantenere i valori per i quali la funzione predicato restituisce al True, quindi bisogna invertire la logica (cioè tu dici "mantieni le persone che non hanno il cognome Smith" invece di "rimuovere le persone che hanno il cognome Smith").

Modifica Divertente ... due persone hanno postato individualmente entrambe le risposte che ho suggerito mentre stavo postando la mia.

  0

Inoltre, espressioni generatore. 07 ott. 082008-10-07 18:50:11

+12

'not name.endswith (" Smith ")' sembra molto più bello :-) 07 dic. 092009-12-07 04:13:47

+5

sicuro, se ti piace la leggibilità o qualcosa del genere. 07 dic. 092009-12-07 22:31:09

  0

Qualcuno può spiegare il '[-5:]' a me. Cosa succede se si desidera controllare l'intera lista? 14 set. 112011-09-14 15:45:49

+2

@Sevenearths: "[-5:]" prende gli ultimi cinque caratteri del nome, poiché vogliamo sapere se il nome termina con "Smith". Come suggerito da Jochen, l'espressione "nome [: - 5]! = 'Smith'" potrebbe essere scritta più facilmente come "non name.endswith ('Smith')". 14 nov. 112011-11-14 14:57:43

  0

Non dimenticare di menzionare l'aumento delle prestazioni usando 'name.endswith (" Smith ")' invece di '[-5:]' 03 lug. 142014-07-03 01:24:11

  0

@ notbad.jpeg: la micro-ottimizzazione non è importante, ma c'è un performance 'diminuire' usando' name.endswith ("Smith") 'rispetto all'indicizzazione. Benchmark (l'ho fatto)! 23 apr. 152015-04-23 15:21:16

  0

@Gerrat Grazie per la correzione. Ho pensato che 'endswith()' avrebbe prestazioni migliori dal momento che è il modo più paragonabile di confrontare e può sfruttare le ottimizzazioni perché non deve fare una copia secondaria della stringa. 29 apr. 152015-04-29 16:05:29


-2

Bene, questo è chiaramente un problema con la struttura dati che si sta utilizzando. Ad esempio, utilizza una tabella hash. Alcune implementazioni supportano più voci per chiave, quindi è possibile inserire l'elemento più recente o rimuoverle tutte.

Ma questo è, e quello che troverai troverà la soluzione, l'eleganza attraverso una diversa struttura dati, non algoritmo. Forse puoi fare meglio se è ordinato, o qualcosa del genere, ma l'iterazione su una lista è il tuo unico metodo qui.

modifica: si realizza che ha chiesto "efficienza" ... tutti questi metodi suggeriti si limitano a scorrere l'elenco, che è lo stesso di quello che ha suggerito.

+1

Per alcuni problemi, passare a una diversa struttura dati non è realmente un'opzione, in particolare se non si conosce la condizione del filtro fino a quando non è stata creata la serie di elementi. Ad esempio, se stai facendo una sorta di ricerca e vuoi sfoltire il tuo spazio di ricerca, generalmente non conoscerai in anticipo le condizioni di taglio appropriate per la tua potatura. 09 gen. 112011-01-09 14:44:26


2
names = filter(lambda x: x[-5:] != "Smith", names); 

3

filtro sarebbe fantastico per questo. Esempio semplice:

names = ['mike', 'dave', 'jim'] 
filter(lambda x: x != 'mike', names) 
['dave', 'jim'] 

Edit: di Corey di lista è impressionante troppo.


10

Utilizzando a list comprehension

list = [x for x in list if x[-5:] != "smith"] 
  0

Sembra non funzionare per i numeri interi. temprevengelist = "0-12354-6876" temprevengelist = temprevengelist.split ('-') list = [x per x in temprevengelist se x [-5:]!= 6876] 28 gen. 102010-01-28 07:48:53

  0

@FahimAkhter: Questo perché si confronta un intero con una stringa: in Python, '6876' (l'int) e' "6876" '(la stringa) sono due valori diversi e non sono uguali. Prova a sostituire 'x [-5:]! = 6876' con' x [-5:]! = "6876" 'o' int (x [-5:])! = 6876' 20 apr. 122012-04-20 19:33:11


2

Entrambe le soluzioni, filtro e comprensione richiede la costruzione di un nuovo elenco. Io non ne so abbastanza delle parti interne Python per essere sicuro, ma io penso che un approccio più tradizionale (ma meno elegante) potrebbe essere più efficiente:

names = ['Jones', 'Vai', 'Smith', 'Perez'] 

item = 0 
while item <> len(names): 
    name = names [item] 
    if name=='Smith': 
     names.remove(name) 
    else: 
     item += 1 

print names 

In ogni caso, per le liste brevi, mi attengo con una delle due soluzioni proposte in precedenza.

  0

Penso che names.remove (nome) potrebbe essere un'operazione O (n), che renderebbe questo un algoritmo O (n^2). 04 ott. 082008-10-04 03:28:27

+1

Scriverò personalmente la mia espressione while come elemento <len (nomi), nel caso in cui avessi rovinato la logica all'interno del ciclo. (anche se non sembra che tu l'abbia fatto) 08 ott. 082008-10-08 00:31:51

  0

Probabilmente è più efficiente usare nomi [item] o names.pop (item) di names.remove (nome). È molto meno probabile che sia O (n), anche se non conosco i reali interni di come funziona. 05 nov. 082008-11-05 13:11:18


1

il filtro e list comprehension sono ok per il tuo esempio, ma hanno un paio di problemi:

  • Fanno una copia della vostra lista e restituire il nuovo, e che sarà inefficiente quando l'originale la lista è veramente grande
  • Possono essere davvero ingombranti quando i criteri per scegliere gli elementi (nel tuo caso, se il nome [-5:] == 'Smith') è più complicato, o ha diverse condizioni.

La tua soluzione originale è in realtà più efficiente per elenchi molto grandi, anche se siamo d'accordo che è più brutto. Ma se ti preoccupi che si può avere di più 'John Smith', può essere fissato eliminando in base alla posizione e non sul valore:

names = ['Jones', 'Vai', 'Smith', 'Perez', 'Smith'] 

toremove = [] 
for pos, name in enumerate(names): 
    if name[-5:] == 'Smith': 
     toremove.append(pos) 
for pos in sorted(toremove, reverse=True): 
    del(names[pos]) 

print names 

Non possiamo scegliere una soluzione senza considerare la dimensione della lista, ma per le grandi liste preferirei la soluzione a 2 passaggi anziché il filtro o le liste di comprensione

  0

Questo non funziona correttamente se si dispone di più di una voce "Smith", poiché le istanze aggiuntive da rimuovere sono state spostate a causa della rimozione di istanze precedenti. E per un motivo simile, questo algoritmo causa un'eccezione se una seconda voce "Smith" viene aggiunta alla fine dell'elenco. 08 ott. 082008-10-08 00:37:46

  0

@Miquella: hai ragione, il mio post originale non è riuscito per più Smith, l'ho risolto facendo l'eliminazione in ordine inverso. Grazie. 10 ott. 082008-10-10 18:12:03


4

Ci sono momenti in cui il filtraggio (usando il filtro o una lista di comprensione) non funziona. Questo accade quando qualche altro oggetto contiene un riferimento alla lista che stai modificando e devi modificare l'elenco sul posto.

for name in names[:]: 
    if name[-5:] == 'Smith': 
     names.remove(name) 

L'unica differenza dal codice originale è l'uso di names[:] anziché names nel ciclo for. In questo modo il codice itera su una copia (superficiale) della lista e le rimozioni funzionano come previsto. Poiché la lista delle copie è superficiale, è abbastanza veloce.


2

per rispondere alla tua domanda su come lavorare con i dizionari, si dovrebbe notare che Python 3.0 includerà dict comprehensions:

>>> {i : chr(65+i) for i in range(4)} 

Nel frattempo, si può fare la comprensione quasi-dict in questo modo:

>>> dict([(i, chr(65+i)) for i in range(4)]) 

O come una risposta più diretta:

dict([(key, name) for key, name in some_dictionary.iteritems if name[-5:] != 'Smith']) 
  0

non c'è bisogno di mettere '()' attorno alle espressioni del generatore a meno che non siano l'unico argomento e '[]' rende l'espressione del generatore materializzare una lista che è ciò che rende 'dict ([(k, v) per k , v in d.items()]) 'molto più lento di' dict (((k, v) per k, v in d.items())) ' 04 mar. 112011-03-04 10:40:38


37

È inoltre possibile iter mangiato all'indietro sopra la lista:

for name in reversed(names): 
    if name[-5:] == 'Smith': 
     names.remove(name) 

Questo ha il vantaggio che non crea una nuova lista (come filter o una lista di comprensione) e utilizza un iteratore invece di una copia dell'elenco (come [:]).

Si noti che sebbene la rimozione degli elementi mentre si esegue l'iterazione all'indietro sia sicura, inserirli è un po 'più complicato.

  0

Risolto il mio problema, grazie :) 24 dic. 082008-12-24 13:03:09

  0

Questo è davvero un soluzione innovativa e pitonica. Lo adoro! 07 dic. 092009-12-07 04:13:11

  0

oo piuttosto intelligente 12 ago. 102010-08-12 17:06:45

  0

Funziona se ci sono duplicati nella lista (che corrispondono al predicato)? 04 set. 122012-09-04 16:58:06

  0

@ Jon-Eric: sì, funziona. Se c'è un duplicato, allora il primo viene rimosso, l'elenco si restringe e il 'reverseed()' restituisce lo stesso 'nome' la seconda volta. È un algoritmo O (n ** 2) diverso da [la risposta accettata] (http://stackoverflow.com/a/18435/4279) che utilizza l'algoritmo O (n). 23 mag. 142014-05-23 21:18:15


1

Nel caso di un set.

toRemove = set([]) 
for item in mySet: 
    if item is unwelcome: 
     toRemove.add(item) 
mySets = mySet - toRemove 

28

La risposta ovvia è quella che John e un paio di altre persone hanno dato, e cioè:

>>> names = [name for name in names if name[-5:] != "Smith"]  # <-- slower 

Ma che ha lo svantaggio che si crea un nuovo oggetto lista, piuttosto che riutilizzare l'oggetto originale . Ho fatto qualche profilazione e la sperimentazione, e il metodo più efficiente sono venuto in mente è:

>>> names[:] = (name for name in names if name[-5:] != "Smith") # <-- faster 

Assegnazione di nomi "[:]" fondamentalmente significa "sostituire il contenuto della lista nomi con il seguente valore". È diverso dall'assegnazione ai nomi, in quanto non crea un nuovo oggetto elenco. Il lato destro del compito è un'espressione di generatore (notare l'uso di parentesi piuttosto che parentesi quadre). Ciò farà in modo che Python iteri su tutta la lista.

Alcuni profili rapidi suggeriscono che questo è circa il 30% più veloce rispetto all'approccio di tipo list comprehension e circa il 40% più veloce rispetto all'approccio filtro.

Caveat: mentre questa soluzione è più veloce della soluzione ovvia, è più oscura e si basa su tecniche Python più avanzate. Se lo usi, ti consiglio di accompagnarlo con un commento. Probabilmente vale la pena utilizzarlo solo nei casi in cui ti interessa davvero le prestazioni di questa particolare operazione (che è piuttosto veloce, non importa quale). (Nel caso in cui l'ho usato, stavo facendo A * ricerca raggio e usato questo per rimuovere i punti di ricerca dal fascio di ricerca.)

+2

Scoperta di prestazioni davvero interessante. Potresti condividere di più sul tuo ambiente di profilazione e sui metodi di valutazione? 02 mar. 122012-03-02 01:39:34

  0

Scommetto che potresti renderlo ancora più veloce usando 'non name.endswith ('Smith')' invece di creare una slice ogni iterazione. Ad ogni modo, questa è un'informazione preziosa che probabilmente non ho mai trovato se non fosse per la tua risposta, grazie. 03 lug. 142014-07-03 01:27:44

+1

il suggerimento 'names [:]' è stato particolarmente utile per l'utilizzo con 'os.walk' per filtrare i nomi dirette per attraversare 18 giu. 152015-06-18 02:49:23


2

Se l'elenco deve essere filtrato sul posto e la dimensione dell'elenco è abbastanza grande quindi gli algoritmi citati nelle risposte precedenti, che sono basati su list.remove(), potrebbero non essere adatti, perché la loro complessità computazionale è O (n^2). In questo caso è possibile utilizzare la seguente funzione non-così divinatorio:

def filter_inplace(func, original_list): 
    """ Filters the original_list in-place. 

    Removes elements from the original_list for which func() returns False. 

    Algrithm's computational complexity is O(N), where N is the size 
    of the original_list. 
    """ 

    # Compact the list in-place. 
    new_list_size = 0 
    for item in original_list: 
    if func(item): 
     original_list[new_list_size] = item 
     new_list_size += 1 

    # Remove trailing items from the list. 
    tail_size = len(original_list) - new_list_size 
    while tail_size: 
    original_list.pop() 
    tail_size -= 1 


a = [1, 2, 3, 4, 5, 6, 7] 

# Remove even numbers from a in-place. 
filter_inplace(lambda x: x & 1, a) 

# Prints [1, 3, 5, 7] 
print a 

Edit: In realtà, la soluzione a https://stackoverflow.com/a/4639748/274937 è superiore a soluzione il mio. È più pitonico e funziona più velocemente. Quindi, ecco un nuovo filter_inplace() implementazione:

def filter_inplace(func, original_list): 
    """ Filters the original_list inplace. 

    Removes elements from the original_list for which function returns False. 

    Algrithm's computational complexity is O(N), where N is the size 
    of the original_list. 
    """ 
    original_list[:] = [item for item in original_list if func(item)] 
  0

per rimuovere gli elementi finali:' del list_originale [new_list_size:] ' 03 mar. 132013-03-03 06:23:45


1

Ecco il mio filter_inplace implementazione che può essere utilizzato per filtrare gli elementi da un elenco sul posto, mi si avvicinò con questo per conto mio in modo indipendente prima di trovare questa pagina . È lo stesso algoritmo di quello che ha pubblicato PabloG, ma è più generico per poterlo utilizzare per filtrare gli elenchi, ma è anche in grado di rimuovere dall'elenco basato sullo comparisonFunc se invertito è impostato su True; una sorta di filtro invertito, se vuoi.

def filter_inplace(conditionFunc, list, reversed=False): 
    index = 0 
    while index < len(list): 
     item = list[index] 

     shouldRemove = not conditionFunc(item) 
     if reversed: shouldRemove = not shouldRemove 

     if shouldRemove: 
      list.remove(item) 
     else: 
      index += 1