¿Cómo elimino duplicados de una lista manteniendo el orden?
¿Cómo elimino duplicados de una lista manteniendo el orden? El uso de un conjunto para eliminar duplicados destruye el orden original. ¿Existe un modismo incorporado o pitónico?
Aquí tienes algunas alternativas: http://www.peterbe.com/plog/uniqifiers-benchmark
El más rápido:
def f7(seq):
seen = set()
seen_add = seen.add
return [x for x in seq if not (x in seen or seen_add(x))]
¿Por qué asignar seen.add
en seen_add
lugar de simplemente llamar seen.add
? Python es un lenguaje dinámico y resolver seen.add
cada iteración es más costoso que resolver una variable local. seen.add
podría haber cambiado entre iteraciones y el tiempo de ejecución no es lo suficientemente inteligente como para descartarlo. Para ir a lo seguro, tiene que comprobar el objeto cada vez.
Si planea utilizar esta función con frecuencia en el mismo conjunto de datos, quizás sería mejor utilizar un conjunto ordenado: http://code.activestate.com/recipes/528878/
O (1) inserción, eliminación y verificación de miembros por operación.
(Pequeña nota adicional: seen.add()
siempre devuelve None
, por lo que lo or
anterior existe solo como una forma de intentar una actualización del conjunto, y no como parte integral de la prueba lógica).
La mejor solución varía según la versión de Python y las limitaciones del entorno:
Python 3.7+ (y la mayoría de los intérpretes que admiten 3.6, como detalle de implementación):
Introducido por primera vez en PyPy 2.5.0 y adoptado en CPython 3.6 como detalle de implementación, antes de convertirse en una garantía de lenguaje en Python 3.7, Plain dict
tiene orden de inserción e incluso es más eficiente que (también C implementado a partir de CPython 3.5) collections.OrderedDict
. Así que la solución más rápida, con diferencia, es también la más sencilla:
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(dict.fromkeys(items)) # Or [*dict.fromkeys(items)] if you prefer
[1, 2, 0, 3]
De list(set(items))
esta manera, se lleva todo el trabajo a la capa C (en CPython), pero dado que dict
los s están ordenados por inserción, dict.fromkeys
no pierde el orden. Es más lento que list(set(items))
(normalmente tarda entre un 50 y un 100% más), pero mucho más rápido que cualquier otra solución para conservar el orden (toma aproximadamente la mitad del tiempo que los hacks que implican el uso de set
s en un listcomp ).
Nota importante : la unique_everseen
solución de more_itertools
(ver más abajo) tiene algunas ventajas únicas en términos de pereza y soporte para elementos de entrada no hash; Si necesita estas funciones, es la única solución que funcionará.
Python 3.5 (y todas las versiones anteriores si el rendimiento no es crítico )
Como señaló Raymond , en CPython 3.5, donde OrderedDict
se implementa en C, los feos trucos de comprensión de listas son más lentos que OrderedDict.fromkeys
(a menos que realmente necesite la lista al final, e incluso entonces, solo si la entrada es muy corta). Entonces, tanto en rendimiento como en legibilidad, la mejor solución para CPython 3.5 es el OrderedDict
equivalente al uso 3.6+ de simple dict
:
>>> from collections import OrderedDict
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(OrderedDict.fromkeys(items))
[1, 2, 0, 3]
En CPython 3.4 y versiones anteriores, esto será más lento que otras soluciones, por lo que si la creación de perfiles muestra que necesita una solución mejor, siga leyendo.
Python 3.4 y versiones anteriores, si el rendimiento es crítico y los módulos de terceros son aceptables
Como señala @abarnert , la more_itertools
biblioteca ( pip install more_itertools
) contiene una unique_everseen
función diseñada para resolver este problema sin mutaciones ilegibles ( not seen.add
) en las listas por comprensión. Esta también es la solución más rápida:
>>> from more_itertools import unique_everseen
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(unique_everseen(items))
[1, 2, 0, 3]
Sólo una simple importación de biblioteca y sin hacks.
El módulo está adaptando la receta de itertools unique_everseen
que se ve así:
def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
for element in filterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
pero a diferencia de la itertools
receta, admite elementos que no se pueden usar con hash (con un costo de rendimiento; si todos los elementos iterable
no son con hash, el algoritmo se convierte en O(n²)
, en comparación O(n)
con si todos son con hash).
Nota importante : a diferencia de todas las demás soluciones aquí, unique_everseen
se puede utilizar de forma perezosa; el uso máximo de memoria será el mismo (eventualmente, el subyacente set
crece al mismo tamaño), pero si no list
especifica el resultado, simplemente lo repite, podrá procesar elementos únicos a medida que se encuentren. en lugar de esperar hasta que se haya deduplicado toda la entrada antes de procesar el primer elemento único.
Python 3.4 y versiones anteriores, si el rendimiento es crítico y los módulos de terceros no están disponibles
Tienes dos opciones:
Copie y pegue la
unique_everseen
receta en su código y úsela según elmore_itertools
ejemplo anterior.Utilice trucos feos para permitir que una única lista de compilación verifique y actualice
set
para realizar un seguimiento de lo que se ha visto:seen = set() [x for x in seq if x not in seen and not seen.add(x)]
a expensas de depender del feo truco :
not seen.add(x)
que se basa en el hecho de que
set.add
es un método local que siempre regresa y se evalúaNone
como .not None
True
Tenga en cuenta que todas las soluciones anteriores lo son O(n)
(salvo la llamada unique_everseen
a un iterable de elementos no hashables, que es O(n²)
, mientras que las otras fallarían inmediatamente con un TypeError
), por lo que todas las soluciones tienen el rendimiento suficiente cuando no son la ruta de código más popular. Cuál usar depende de en qué versiones de la especificación del lenguaje/intérprete/módulos de terceros puede confiar, si el rendimiento es crítico o no (no asuma que lo es; generalmente no lo es) y, lo más importante, la legibilidad. (porque si la persona que mantiene este código termina más tarde con un humor asesino, su inteligente microoptimización probablemente no valió la pena).
En CPython 3.6+ (y todas las demás implementaciones de Python que comienzan con Python 3.7+ ), los diccionarios están ordenados , por lo que la forma de eliminar duplicados de un iterable manteniéndolo en el orden original es:
>>> list(dict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']
En Python 3.5 y versiones anteriores (incluido Python 2.7 ), utilice el archivo OrderedDict
. Mis tiempos muestran que este es ahora el más rápido y el más corto de los diversos enfoques para Python 3.5 (cuando obtuvo una implementación en C; antes de 3.5 sigue siendo la solución más clara, aunque no la más rápida).
>>> from collections import OrderedDict
>>> list(OrderedDict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']
En Python 3.7 y superiores, se garantiza que los diccionarios recordarán el orden de inserción de sus claves. La respuesta a esta pregunta resume la situación actual.
Por lo tanto, la OrderedDict
solución queda obsoleta y sin declaraciones de importación simplemente podemos emitir:
>>> lst = [1, 2, 1, 3, 3, 2, 4]
>>> list(dict.fromkeys(lst))
[1, 2, 3, 4]
No es por patear un caballo muerto (esta pregunta es muy antigua y ya tiene muchas buenas respuestas), pero aquí hay una solución que utiliza pandas que es bastante rápida en muchas circunstancias y muy sencilla de usar.
import pandas as pd
my_list = [0, 1, 2, 3, 4, 1, 2, 3, 5]
>>> pd.Series(my_list).drop_duplicates().tolist()
# Output:
# [0, 1, 2, 3, 4, 5]