2008-08-20 15 views
50

Cuando estoy escribiendo código en Python, a menudo necesito eliminar elementos de una lista u otro tipo de secuencia en función de algunos criterios. No he encontrado una solución que sea elegante y eficiente, ya que eliminar elementos de una lista que está iterando actualmente es malo. Por ejemplo, no se puede hacer esto:¿Manera elegante de eliminar elementos de la secuencia en Python?

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

Por lo general terminan haciendo algo como esto:

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

Ésta es innefficient, bastante feo y posiblemente con errores (¿cómo manejar múltiples 'John Smith 'entradas?). ¿Alguien tiene una solución más elegante, o al menos una más eficiente?

¿Qué tal uno que funciona con los diccionarios?

+0

Su código elimina múltiples Smiths o lo editó? – systemovich

Respuesta

52

Dos maneras fáciles de lograr casi el filtrado son:

  1. Usando filter:

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

  2. Uso de listas por comprensión:

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

Tenga en cuenta que ambos casos mantener los valores para los que la función de predicado evalúa como True, así que hay que invertir la lógica (es decir, dices "mantener a las personas que no tienen el apellido Smith" en lugar de "eliminar a las personas que tienen el apellido Smith").

Editar Gracioso ... dos personas publicaron de forma individual las dos respuestas que sugerí cuando estaba publicando la mía.

+0

También expresiones de generador. –

+12

'not name.endswith (" Smith ")' se ve mucho mejor :-) –

+5

seguro, si te gusta la legibilidad o algo así. – John

-2

Bueno, esto es claramente un problema con la estructura de datos que está utilizando. Use una tabla hash por ejemplo. Algunas implementaciones admiten entradas múltiples por clave, por lo que uno puede mostrar el elemento más nuevo o eliminarlos todos.

Pero esto es, y lo que vas a encontrar la solución es elegancia a través de una estructura de datos diferente, no de algoritmo. Tal vez puedas mejorar si está ordenado, o algo así, pero la iteración en una lista es tu único método aquí.

edit: uno se da cuenta que pidió 'eficiencia' ... todos estos métodos sugeridos simplemente iteran sobre la lista, que es la misma que sugirió.

+1

Para algunos problemas, cambiar a una estructura de datos diferente no es realmente una opción, en particular, si no conoce la condición del filtro hasta después de que se haya creado el conjunto de elementos. Por ejemplo, si está haciendo algún tipo de búsqueda y desea podar su espacio de búsqueda, generalmente no sabrá la condición de corte adecuada para su poda por adelantado. –

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

filtro sería genial para esto. Un simple ejemplo:

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

Editar: lista por comprensión de Corey es increíble.

10

Usando a list comprehension

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

Parece que no funciona para enteros. temprevengelist = "0-12354-6876" temprevengelist = temprevengelist.split ('-') list = [x para x en temprevengelist si x [-5:]!= 6876] –

+0

@FahimAkhter: Eso es porque estás comparando un entero con una cadena: en Python, '6876' (el int) y' "6876" '(la cadena) son dos valores diferentes, y no son iguales. Intente reemplazar 'x [-5:]! = 6876' con' x [-5:]! = "6876" 'o' int (x [-5:])!! = 6876' –

2

Ambas soluciones, filtro y de comprensión requiere la construcción de una nueva lista. No sé lo suficiente de las partes internas de Python para estar seguro, pero yo creo que un enfoque más tradicional (pero menos elegante) podría ser más eficiente:

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 

De todos modos, para las listas cortas, me quedo con cualquiera de las dos soluciones propuestas anteriormente.

+0

Creo que names.remove (name) podría ser una operación O (n), lo que haría de esto un algoritmo O (n^2). – postfuturist

+1

Escribo personalmente mi expresión while como item Miquella

+0

Probablemente sea más eficiente usar los nombres del [elemento] o los nombres.pop (elemento) que names.remove (nombre). Es mucho menos probable que sea O (n), aunque no conozco las partes internas reales de cómo funciona. – rjmunro

1

Las comprensiones de filtro y de la lista están bien, por su ejemplo, pero tienen un par de problemas:

  • Hacen una copia de su lista y regresar al nuevo, y que serán ineficaces cuando el original la lista es realmente grande
  • Pueden ser realmente engorrosos cuando los criterios para elegir elementos (en su caso, si el nombre [-5:] == 'Smith') es más complicado o tiene varias condiciones.

Su solución original es en realidad más eficiente para listas muy grandes, incluso si podemos estar de acuerdo es más feo. Pero si usted se preocupe que usted puede tener múltiples 'John Smith', que se puede fijar mediante la supresión basada en la posición y no en el valor:

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 

No podemos elegir una solución sin tener en cuenta el tamaño de la lista, pero para listas grandes, preferiría su solución de 2 pases en lugar del filtro o listas de comprensiones

+0

Esto no funciona correctamente si tiene más de una entrada 'Smith', porque las instancias adicionales para eliminar se han cambiado debido a la eliminación de instancias anteriores. Y por una razón similar, este algoritmo causa una excepción si se agrega una segunda entrada 'Smith' al final de la lista. – Miquella

+0

@Miquella: tiene razón, mi publicación original falló para múltiples Smiths, lo arreglé haciendo la eliminación en orden inverso. Gracias. –

4

Hay momentos en que el filtrado (ya sea utilizando filtro o una lista de comprensión) no funciona. Esto sucede cuando otro objeto contiene una referencia a la lista que está modificando y necesita modificar la lista en su lugar.

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

La única diferencia con el código original es el uso de names[:] en lugar de names en el bucle. De esta forma, el código itera sobre una copia (poco profunda) de la lista y las eliminaciones funcionan como se espera. Como la copia de listas es superficial, es bastante rápida.

2

Para responder a su pregunta sobre el trabajo con los diccionarios, debe tener en cuenta que Python 3.0 incluirá dict comprehensions:

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

Por el momento, se puede hacer una comprensión cuasi-dict esta manera:

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

O como una respuesta más directa:

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

no necesita poner '()' alrededor de las expresiones del generador a menos que no sean el único argumento y '[]' haga que la expresión del generador materialice una lista que es lo que hace 'dict ([(k, v) para k , v en d.items()]) 'mucho más lento que' dict (((k, v) para k, v en d.items())) ' –

37

también puede iter comió hacia atrás sobre la lista:

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

Esto tiene la ventaja de que no crea una nueva lista (como filter o una lista por comprensión) y utiliza un iterador en lugar de una copia lista (como [:]).

Tenga en cuenta que, aunque la eliminación de elementos mientras se itera hacia atrás es segura, insertarlos es un poco más complicado.

+0

Resolvió mi problema, gracias :) – Ashy

+0

Esto es realmente solución innovadora y Pythonic. ¡Lo amo! – richo

+0

oo bastante inteligente – Claudiu

1

En el caso de un conjunto.

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

La respuesta obvia es la que John y un par de otras personas dieron, a saber:

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

pero que tiene el inconveniente de que se crea un nuevo objeto de lista, en lugar de volver a utilizar el objeto original . He hecho un poco de perfiles y la experimentación, y el método más eficiente que se me ocurrió es:

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

Asignación de nombres "[:]" básicamente significa "sustituir el contenido de la lista de nombres con el siguiente valor". Es diferente de solo asignarle nombres, ya que no crea un nuevo objeto de lista. El lado derecho de la tarea es una expresión generadora (tenga en cuenta el uso de paréntesis en lugar de corchetes). Esto hará que Python itere en la lista.

Algunos perfiles rápidos sugieren que esto es aproximadamente un 30% más rápido que el enfoque de comprensión de listas, y un 40% más rápido que el enfoque de filtro.

Advertencia: aunque esta solución es más rápida que la solución obvia, es más oscura y se basa en técnicas de Python más avanzadas. Si lo usa, le recomiendo que lo acompañe con un comentario. Probablemente solo valga la pena usarlo en casos en los que realmente se preocupe por el rendimiento de esta operación en particular (lo cual es bastante rápido sin importar qué). (En el caso donde lo usé, estaba haciendo una búsqueda de haz A * y lo usé para eliminar puntos de búsqueda del haz de búsqueda.)

+2

descubrimiento de rendimiento realmente interesante. ¿Podría compartir más sobre su entorno de creación de perfiles y métodos de evaluación? – Drake

+0

Apuesto a que podrías hacerlo aún más rápido usando 'not name.endswith ('Smith')' en lugar de crear un slice en cada iteración. De cualquier manera, esta es una valiosa información que probablemente nunca encontré si no fuera por tu respuesta, gracias. –

+1

la sugerencia 'names [:]' fue particularmente útil para usar con 'os.walk' para filtrar dirnames a través de – wowest

2

Si la lista debe filtrarse in situ y el tamaño de la lista es bastante grande , entonces los algoritmos mencionados en las respuestas anteriores, que se basan en list.remove(), pueden ser inadecuados, porque su complejidad computacional es O (n^2). En este caso se puede usar la siguiente función no-tan Pythonic:

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 

Editar: En realidad, la solución al https://stackoverflow.com/a/4639748/274937 es superior a la solución de la mía. Es más pitónico y funciona más rápido. Por lo tanto, aquí hay una nueva filter_inplace() aplicación:

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

para eliminar elementos finales:' del original_list [new_list_size:] ' – jfs

1

Aquí está mi aplicación filter_inplace que se puede utilizar para filtrar los elementos de una lista en contexto, se me ocurrió esto en mi propia forma independiente antes de encontrar esta página . Es el mismo algoritmo que PabloG publicó, simplemente se hizo más genérico para que pueda usarlo para filtrar las listas en su lugar, también se puede eliminar de la lista según el comparisonFunc si se ha invertido True; una especie de filtro invertido si se quiere.

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 
Cuestiones relacionadas