Wie entferne ich Duplikate aus einer Liste, während ich die Reihenfolge beibehalte?

Lesezeit: 8 Minuten

Benutzer-Avatar
Josh Glover

Wie entferne ich Duplikate aus einer Liste, während ich die Reihenfolge beibehalte? Die Verwendung eines Sets zum Entfernen von Duplikaten zerstört die ursprüngliche Reihenfolge. Gibt es ein eingebautes oder ein pythonisches Idiom?

Verwandte Frage: Was ist in Python der schnellste Algorithmus zum Entfernen von Duplikaten aus einer Liste, sodass alle Elemente eindeutig sind? unter Wahrung der Ordnung?

  • Vielleicht möchten Sie die 2020-Bearbeitung dieser Antwort in Betracht ziehen: stackoverflow.com/a/17016257/1219006, die jetzt die beste Lösung für Python 3.6 (Cpython)-7 (alle Pythons) + zu sein scheint list(dict.fromkeys(items))

    – Jamylak

    14. Dezember 2020 um 5:08 Uhr


Die beste Lösung variiert je nach Python-Version und Umgebungseinschränkungen:

Python 3.7+ (und die meisten Interpreter, die 3.6 unterstützen, als Implementierungsdetail):

Zuerst in PyPy 2.5.0 eingeführt und in CPython 3.6 als Implementierungsdetail übernommen, bevor es in Python 3.7, plain, zu einer Sprachgarantie gemacht wurde dict ist einfügungsgeordnet und sogar effizienter als die (ebenfalls in C implementiert ab CPython 3.5) collections.OrderedDict. Die bei weitem schnellste Lösung ist also auch die einfachste:

>>> items = [1, 2, 0, 1, 3, 2]
>>> list(dict.fromkeys(items))  # Or [*dict.fromkeys(items)] if you prefer
[1, 2, 0, 3]

Wie list(set(items)) Dies verschiebt die gesamte Arbeit auf die C-Schicht (auf CPython), aber seitdem dicts sind nach Einfügung geordnet, dict.fromkeys verliert nicht die Bestellung. Es ist langsamer als list(set(items)) (dauert normalerweise 50-100 % länger), aber viel schneller als jede andere ordnungserhaltende Lösung (dauert etwa die Hälfte der Zeit von Hacks mit Verwendung von sets in einem listcomp).

Wichtiger Hinweis: Das unique_everseen Lösung aus more_itertools (siehe unten) hat einige einzigartige Vorteile in Bezug auf Faulheit und Unterstützung für nicht hashfähige Eingabeelemente; Wenn Sie diese Funktionen benötigen, ist es das nur Lösung, die funktionieren wird.

Python 3.5 (und alle älteren Versionen, wenn die Leistung es nicht ist kritisch)

Wie Raymond betonte, in CPython 3.5 wo OrderedDict in C implementiert ist, sind hässliche Listenverständnis-Hacks langsamer als OrderedDict.fromkeys (es sei denn, Sie brauchen die Liste wirklich am Ende – und auch dann nur, wenn die Eingabe sehr kurz ist). Sowohl hinsichtlich der Leistung als auch der Lesbarkeit ist die beste Lösung für CPython 3.5 die OrderedDict Äquivalent der 3.6+ Verwendung von plain dict:

>>> from collections import OrderedDict
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(OrderedDict.fromkeys(items))
[1, 2, 0, 3]

Auf CPython 3.4 und früheren Versionen ist dies langsamer als bei einigen anderen Lösungen. Wenn also die Profilerstellung zeigt, dass Sie eine bessere Lösung benötigen, lesen Sie weiter.

Python 3.4 und früher, wenn die Leistung kritisch ist und Module von Drittanbietern akzeptabel sind

Wie @abarnert feststellt, die more_itertools Bibliothek (pip install more_itertools) enthält ein unique_everseen Funktion, die gebaut wurde, um dieses Problem ohne irgendwelche zu lösen unlesbar (not seen.add) Mutationen im Listenverständnis. Dies ist auch die schnellste Lösung:

>>> from more_itertools import unique_everseen
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(unique_everseen(items))
[1, 2, 0, 3]

Nur ein einfacher Bibliotheksimport und keine Hacks.

Das Modul passt das itertools-Rezept an unique_everseen das sieht aus wie:

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

aber anders als die itertools Rezept unterstützt es nicht hashfähige Elemente (auf Kosten der Leistung; wenn alle Elemente in iterable nicht hashfähig sind, wird der Algorithmus O(n²)vs. O(n) wenn sie alle hashbar sind).

Wichtiger Hinweis: Im Gegensatz zu allen anderen Lösungen hier, unique_everseen kann faul verwendet werden; die maximale Speicherauslastung wird dieselbe sein (schließlich die zugrunde liegende set auf die gleiche Größe wächst), aber wenn Sie dies nicht tun listWenn Sie das Ergebnis verifizieren, iterieren Sie es einfach, Sie können eindeutige Elemente verarbeiten, sobald sie gefunden werden, anstatt zu warten, bis die gesamte Eingabe dedupliziert wurde, bevor Sie das erste eindeutige Element verarbeiten.

Python 3.4 und früher, wenn die Leistung kritisch ist und Module von Drittanbietern sind nicht verfügbar

Sie haben zwei Möglichkeiten:

  1. Kopieren und einfügen das unique_everseen Rezept zu Ihrem Code und verwenden Sie es pro die more_itertools Beispiel oben

  2. Verwenden Sie hässliche Hacks, um einem einzelnen listcomp sowohl das Überprüfen als auch das Aktualisieren von a zu ermöglichen set um zu verfolgen, was gesehen wurde:

    seen = set()
    [x for x in seq if x not in seen and not seen.add(x)]
    

    auf Kosten des Vertrauens auf die hässlicher Hack:

     not seen.add(x)
    

    was darauf beruht, dass set.add ist eine In-Place-Methode, die immer zurückkehrt None Also not None wertet zu True.

Beachten Sie, dass alle der obigen Lösungen sind O(n) (Spart den Anruf unique_everseen auf einem Iterable von nicht hashbaren Elementen, das heißt O(n²)während die anderen sofort mit a fehlschlagen würden TypeError), sodass alle Lösungen leistungsfähig genug sind, wenn sie nicht der heißeste Codepfad sind. Welche Sie verwenden sollten, hängt davon ab, auf welche Versionen der Sprachspezifikation/Interpreter/Drittanbieter-Module Sie sich verlassen können, ob die Leistung kritisch ist oder nicht (nehmen Sie nicht an, dass dies normalerweise nicht der Fall ist) und vor allem die Lesbarkeit (Denn wenn die Person, die diesen Code pflegt, später in einer mörderischen Stimmung endet, hat sich Ihre clevere Mikrooptimierung wahrscheinlich nicht gelohnt).

  • Konvertieren Sie in eine benutzerdefinierte Art von Diktat, nur um Schlüssel zu nehmen? Nur eine weitere Krücke.

    – Nakilon

    14. Juni 2013 um 13:40 Uhr

  • @ Nakilon Ich verstehe nicht wirklich, wie es eine Krücke ist. Es legt keinen veränderlichen Zustand offen, also ist es in diesem Sinne sehr sauber. Intern werden Python-Sätze mit dict() (stackoverflow.com/questions/3949310/…) implementiert, also tun Sie im Grunde nur das, was der Interpreter sowieso getan hätte.

    – Imran

    18. Juni 2013 um 6:58 Uhr


  • @EMS Das bewahrt die Ordnung nicht. Du könntest es genauso gut tun seen = set(seq).

    – Flornbeben

    10. September 2013 um 0:59 Uhr

  • Diese Lösung ist extrem langsamer als der erwähnte “Hack”. Für meine Liste mit 300.000 Einträgen über 50x langsamer.

    – Benutzer136036

    24. Oktober 2014 um 16:23 Uhr

  • @CommuSoft Ich stimme zu, obwohl es aufgrund des höchst unwahrscheinlichen schlimmsten Falls praktisch immer O (n) ist

    – Jamylak

    20. Mai 2015 um 5:22 Uhr

  • Das einzige Problem ist, dass die iterierbaren “Elemente” hashbar sein müssen – es wäre schön, das Äquivalent für iterierbare Elemente mit beliebigen Elementen (als Liste von Listen) zu haben.

    – Herr_und_Frau_D

    31. Mai 2018 um 12:37 Uhr

  • Die Iteration der Insertionsreihenfolge über ein Diktat bietet Funktionen, die mehr Anwendungsfälle bedienen als das Entfernen von Duplikaten. Darauf stützen sich beispielsweise wissenschaftliche Analysen reproduzierbar Berechnungen, die nicht-deterministische Diktiterationen nicht unterstützen. Reproduzierbarkeit ist ein wichtiges aktuelles Ziel in der computergestützten wissenschaftlichen Modellierung, daher begrüßen wir diese neue Funktion. Obwohl ich weiß, dass es trivial ist, mit einem deterministischen Diktat zu bauen, einem leistungsstarken, deterministischen set() würde naiven Benutzern helfen, reproduzierbare Codes zu entwickeln.

    – Arthur

    9. Januar 2019 um 23:01 Uhr


  • Was ist mit der Verwendung [*dict.fromkeys('abracadabra')] (Entpacken) anstatt die Funktion aufzurufen list(...)? In meinen Tests geht das schneller, obwohl nur sehr kleine Unterschiede feststellbar sind. Ich bin mir also nicht sicher, ob das nur ein Zufall ist.

    – colidyre

    24. Juni 2020 um 12:57 Uhr

  • @colidyre Ja, das würde funktionieren. Der geringe Geschwindigkeitsunterschied ist wahrscheinlich darauf zurückzuführen, dass die Bediener keine eingebaute Funktion suchen müssen. Es ist auch eine Frage der Klarheit zu berücksichtigen.

    – Raymond Hettinger

    24. Juni 2020 um 21:50 Uhr

  • @RaymondHettinger: Die Suchkosten waren gering (wurden mit 3,8 kleiner LOAD_GLOBAL); Der Hauptvorteil bestand darin, Konstruktorcodepfade zu vermeiden (erfordert die Erstellung einer tuple zum args und vorbei NULL Zeiger als kwargs dictdann nennen beide die meistens leer __new__ und die __init__ separat, wobei letzteres dann den allgemeinen Argument-Parsing-Code durchlaufen muss, um alle 0-1 Positionsargumente zu übergeben). Ab 3.9 jedoch list() umgeht das meiste davon über das Vectorcall-Protokoll und reduziert den inkrementellen Vorteil von 60-70 ns (3.8.5) auf 20-30 ns (3.10.0) auf meiner Maschine.

    – ShadowRanger

    27. Dezember 2021 um 18:23 Uhr

Benutzer-Avatar
Alexander

Um kein totes Pferd zu treten (diese Frage ist sehr alt und hat bereits viele gute Antworten), aber hier ist eine Lösung mit Pandas, die unter vielen Umständen ziemlich schnell und kinderleicht zu verwenden ist.

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]

  • nützlich, behält aber die Reihenfolge nicht bei. more_itertools.unique_everseen tut.

    – Baxx

    20. Juni 2021 um 12:12 Uhr

Im Python 3.7 und darüber sind Wörterbücher garantiert um sich an ihre Schlüsseleinfügungsreihenfolge zu erinnern. Die Antwort auf diese Frage fasst den aktuellen Stand der Dinge zusammen.

Das OrderedDict Lösung wird damit obsolet und wir können ohne irgendwelche Import-Anweisungen einfach ausgeben:

>>> lst = [1, 2, 1, 3, 3, 2, 4]
>>> list(dict.fromkeys(lst))
[1, 2, 3, 4]

  • nützlich, behält aber die Reihenfolge nicht bei. more_itertools.unique_everseen tut.

    – Baxx

    20. Juni 2021 um 12:12 Uhr

Benutzer-Avatar
Ehrlich abe

sequence = ['1', '2', '3', '3', '6', '4', '5', '6']
unique = []
[unique.append(item) for item in sequence if item not in unique]

einzigartig → ['1', '2', '3', '6', '4', '5']

  • Es ist erwähnenswert, dass dies einläuft n^2

    – Schleifenbiene

    19. März 2014 um 17:13 Uhr

  • Ick. 2 Strikes: Verwenden einer Liste zum Testen der Mitgliedschaft (langsam, O(N) für jede Prüfung) und Verwenden eines Listenverständnisses für die Nebenwirkungen (Erstellen einer weiteren Liste von NoneReferenzen im Prozess!)

    – Martijn Pieters

    3. März 2015 um 14:32 Uhr


  • Ich stimme @MartijnPieters zu, da gibt es absolut nein Grund für das Listenverständnis mit Nebenwirkungen. Verwenden Sie einfach eine for Schleife stattdessen

    – Jamylak

    17. Februar 2018 um 7:43 Uhr

1144270cookie-checkWie entferne ich Duplikate aus einer Liste, während ich die Reihenfolge beibehalte?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy