¿Cómo elimino duplicados de una lista manteniendo el orden?

Resuelto Josh Glover asked hace 15 años • 31 respuestas

¿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?

Josh Glover avatar Jan 26 '09 22:01 Josh Glover
Aceptado

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.adden seen_addlugar de simplemente llamar seen.add? Python es un lenguaje dinámico y resolver seen.addcada iteración es más costoso que resolver una variable local. seen.addpodrí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 oranterior existe solo como una forma de intentar una actualización del conjunto, y no como parte integral de la prueba lógica).

Markus Jarderot avatar Jan 26 '2009 15:01 Markus Jarderot

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 dicttiene 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 dictlos s están ordenados por inserción, dict.fromkeysno 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 sets en un listcomp ).

Nota importante : la unique_everseensolució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 OrderedDictse 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 OrderedDictequivalente 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_itertoolsbiblioteca ( pip install more_itertools) contiene una unique_everseenfunció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_everseenque 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 itertoolsreceta, admite elementos que no se pueden usar con hash (con un costo de rendimiento; si todos los elementos iterableno 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_everseense puede utilizar de forma perezosa; el uso máximo de memoria será el mismo (eventualmente, el subyacente setcrece al mismo tamaño), pero si no listespecifica 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:

  1. Copie y pegue la unique_everseenreceta en su código y úsela según el more_itertoolsejemplo anterior.

  2. Utilice trucos feos para permitir que una única lista de compilación verifique y actualice setpara 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.addes un método local que siempre regresa y se evalúa Nonecomo .not NoneTrue

Tenga en cuenta que todas las soluciones anteriores lo son O(n)(salvo la llamada unique_everseena 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).

jamylak avatar Jun 10 '2013 02:06 jamylak

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']
Raymond Hettinger avatar Oct 03 '2016 15:10 Raymond Hettinger

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 OrderedDictsolució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]
timgeb avatar Mar 02 '2018 08:03 timgeb

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]
Alexander avatar Aug 18 '2017 00:08 Alexander