Список Python – это упорядоченная последовательность, которая может содержать дубликаты значений. В некоторых приложениях может потребоваться список, содержащий только уникальные элементы. В Python удалить дубликаты из списка можно разными способами.
Выбор правильного решения зависит от того, нужно ли приложению сохранять порядок элементов списка после удаления дубликатов. Решения, представленные в этом руководстве, различаются также по читабельности и сложности, а некоторые из них опираются на функции в сторонних модулях.
Содержание
- Что такое дубликат значения?
- Удаление дубликатов из списка с помощью множества
- Как удалить дубликаты из списка с сохранением порядка элементов?
- Другие способы удалить дубликаты из списка
- Что лучше использовать: dict.fromkeys() или множество?
- Заключение
Что такое дубликат значения?
Перед тем как представить решения по удалению дубликатов из списка, полезно пояснить, что мы имеем в виду под дубликатами. Два объекта обычно считаются дубликатами, если их значения равны. В Python оператор равенства ==
возвращает True, если два объекта имеют одинаковое значение. При этом могут быть равны объекты разных типов данных:
print(10.0 == 10) print(1 == True)
True True
Число с плавающей точкой 10.0 и целое число 10 равны. Также равны целое число 1 и булево значение True. В большинстве сценариев эти значения считаются дубликатами.
Некоторые структуры данных Python обеспечивают уникальные значения. Множество – это коллекция, содержащая только уникальные элементы:
print({10, 10.0}) print({10.0, 10}) print({1, True, 1.0})
{10} {10.0} {1}
Этот код создает три множества, используя нотацию {}
. В каждое из этих множеств мы добавляем объекты, которые равны друг другу. Но если во множество добавляются равные объекты, пускай даже и разных типов данных, то в него включается только первый объект.
Помимо элементов множества, в Python уникальными должны быть и ключи словаря:
print({10: "Integer 10", 10.0: "Float 10"}) print({True: "Boolean True", 1: "Integer 1"})
{10: 'Float 10'} {True: 'Integer 1'}
Словари в этом коде содержат одинаковые ключи. Сохраняется только первый ключ. Однако его значение заменяется значением, связанным с равным ключом, которое было добавлено в словарь последним. Поэтому в первом примере сохраненным ключом является целое число 10, а его значением – строка “Float 10”
. Во втором примере ключ True приобретает значение “Integer 1”
.
Поскольку множества и словари требуют уникальных элементов или ключей, они будут использоваться в нескольких решениях этого руководства.
Удаление дубликатов из списка с помощью множества
Самая простая техника удаления дубликатов из списка – это преобразование списка в множество и обратно в список. Множество может содержать только уникальные значения. Поэтому при добавлении в множество дубликаты отбрасываются:
names = ["James", "Bob", "James", "Mark", "Kate", "Sarah", "Kate"] print(list(set(names)))
['Mark', 'Bob', 'Kate', 'James', 'Sarah']
Давайте рассмотрим шаги, необходимые для удаления дубликатов из списка с помощью этой техники:
- Список преобразуется в множество, из которого удаляются все дубликаты.
- Множество возвращается в новый список, содержащий только уникальные значения.
Имена “James” и “Kate” в исходном списке встречаются дважды, но только по одному разу в итоговом.
Однако множества – это неупорядоченные коллекции. Порядок элементов в множестве не сохраняется. Обратите внимание, что имена в итоговом списке расположены не в том же порядке, что и в исходном списке. При повторном выполнении или при использовании другого интерпретатора Python тот же код может выдать другой порядок, поскольку порядок элементов в множестве не гарантируется.
Это решение идеально, когда потеря информации о порядке элементов в списке допустима. Но если порядок элементов должен быть сохранен, нам нужны другие решения.
Как удалить дубликаты из списка с сохранением порядка элементов?
Ключи словарей похожи на элементы множества, поскольку они должны быть уникальными. В словаре могут быть дублирующиеся значения, но не дублирующиеся ключи.
До Python 3.6 словари не сохраняли порядок элементов. Но в качестве побочного эффекта изменений, внесенных в реализацию словарей в Python 3.6, теперь словари сохраняют порядок добавления элементов. В Python 3.6 это было просто деталью реализации, но в Python 3.7 стало формальной частью языка.
Функция dict.fromkeys()
создает словарь из итерируемого объекта. Элементы итерируемого объекта становятся ключами нового словаря:
names = ["James", "Bob", "James", "Mark", "Kate", "Sarah", "Kate"] print(dict.fromkeys(names))
{'James': None, 'Bob': None, 'Mark': None, 'Kate': None, 'Sarah': None}
Поскольку ключи в словаре уникальны, при создании словаря дубликаты значений отбрасываются. При использовании Python 3.7 или более поздней версии гарантируется, что ключи будут располагаться в том же порядке, что и элементы в списке. По умолчанию все ключи имеют значение None. Однако значения не обязательны, так как этот словарь может быть приведен обратно к списку:
names = ["James", "Bob", "James", "Mark", "Kate", "Sarah", "Kate"] print(list(dict.fromkeys(names)))
['James', 'Bob', 'Mark', 'Kate', 'Sarah']
При преобразовании словаря в список используются только ключи. Эта техника преобразует исходный список в новый список без дубликатов и с сохранением исходного порядка элементов.
Другие способы удалить дубликаты из списка
Варианты, представленные в первой части этого руководства, охватывают два сценария удаления дубликатов из списка:
- Использование множества, если порядок элементов не важен.
- Использование
dict.fromkeys()
, если нужно сохранить порядок элементов.
Эти два варианта, вероятно, будут наилучшими решениями в большинстве ситуаций. Однако есть и другие методы удаления дубликатов из списка.
Использование цикла для заполнения нового списка
Простой вариант – проитерировать исходный список и добавить новые элементы в новый список:
names = ["James", "Bob", "James", "Mark", "Kate", "Sarah", "Kate"] unique_names = [] for name in names: if name not in unique_names: unique_names.append(name) print(unique_names)
['James', 'Bob', 'Mark', 'Kate', 'Sarah']
Новый список unique_names
инициализируется как пустой список перед циклом for
. Элемент добавляется в этот новый список, если его в нем еще нет. Порядок элементов в исходном списке сохраняется, поскольку цикл for
просматривает список по порядку.
Это решение может быть неэффективным для больших списков. Рассмотрим более длинный список, состоящий из случайных чисел. Мы можем сравнить производительность этого решения с версией, использующей dict.fromkeys()
:
import random import timeit data = [random.randint(0, 100) for _ in range(100)] use_fromkeys = "unique_data = list(dict.fromkeys(data))" use_for_loop = """unique_data = [] for item in data: if item not in unique_data: unique_data.append(item)""" from_keys_time = timeit.timeit(use_fromkeys, globals=globals()) for_loop_time = timeit.timeit(use_for_loop, globals=globals()) print(f"Time for 'dict.fromkeys()' version: {from_keys_time:0.2f}s") print(f"Time for loop version: {for_loop_time:0.2f}s")
Time for 'dict.fromkeys()' version: 2.26s Time for loop version: 17.88s
Функция timeit()
в одноименном модуле засекает время выполнения предложения (англ. statement). Предложения передаются в функцию timeit.timeit()
в виде строк. Вывод показывает, что версия с циклом for
работает значительно медленнее, чем версия с использованием dict.fromkeys()
. Время выполнения будет отличаться на разных компьютерах и с разными настройками, но вариант с dict.fromkeys()
всегда будет значительно быстрее.
Использование представления списка со множеством
Цикл for
в предыдущем разделе нельзя преобразовать в представление списка, поскольку оператор if
проверяет, не был ли элемент уже добавлен в новый список. Для использования представления списка требуется отдельная структура данных. Для отслеживания элементов, которые уже были добавлены в список, можно использовать множество:
names = ["James", "Bob", "James", "Mark", "Kate", "Sarah", "Kate"] items = set() unique_names = [name for name in names if not (name in items or items.add(name))] print(unique_names)
['James', 'Bob', 'Mark', 'Kate', 'Sarah']
В нашем руководстве это решение показано для полноты картины, однако этот вариант не отличается удобством чтения и не дает прироста производительности по сравнению с использованием dict.fromkeys()
.
Если имя уже есть во множестве items
, то выражение в круглых скобках в представлении списка оценивается как True. Выражение после ключевого слова or
не оценивается, если первый операнд равен True, поскольку or замыкает оценку. Поскольку выражение в круглых скобках равно True, предложение if
равно False, и имя не добавляется в unique_names
.
Если имени нет в элементах множества, выражение после ключевого слова or оценивается и добавляет имя во множество. Однако метод .add()
возвращает None, а это значение является ложным. Теперь выражение if
равно True, и имя добавляется в список unique_names
.
Использование сторонних библиотек
В ряде сторонних модулей есть инструменты для удаления дубликатов. В сторонней библиотеке more_itertools есть функция unique_everseen()
, которая возвращает итератор, содержащий уникальные элементы с сохранением их порядка.
Библиотеку more_itertools можно установить в терминал с помощью pip или других менеджеров пакетов:
$ pip install more_itertools
Теперь мы можем использовать more_itertools.unique_everseen()
для удаления дубликатов из списка:
import more_itertools names = ["James", "Bob", "James", "Mark", "Kate", "Sarah", "Kate"] unique_names = list(more_itertools.unique_everseen(names)) print(unique_names)
['James', 'Bob', 'Mark', 'Kate', 'Sarah']
Поскольку unique_everseen()
возвращает итератор, его вывод приводится к списку.
Еще одна популярная сторонняя библиотека – NumPy, которая также предлагает решение для удаления дубликатов. NumPy можно установить с помощью pip или других менеджеров пакетов:
$ pip install numpy
Функция NumPy unique()
возвращает массив NumPy с уникальными элементами из переданного функции аргумента. Этот массив NumPy можно преобразовать в список с помощью функции NumPy .tolist()
:
import numpy as np names = ["James", "Bob", "James", "Mark", "Kate", "Sarah", "Kate"] unique_names = np.unique(names).tolist() print(unique_names)
['Bob', 'James', 'Kate', 'Mark', 'Sarah']
Однако обратите внимание, что функция NumPy’s unique()
не сохраняет порядок элементов. Вместо этого она возвращает отсортированные элементы. Другая популярная сторонняя библиотека, Pandas, также имеет функцию unique()
, которая работает аналогично, но сохраняет порядок элементов.
Решения на основе NumPy и Pandas идеальны, когда эти библиотеки уже используются в кодовой базе и их не нужно устанавливать и импортировать, чтобы просто удалить дубликаты из списка.
Что лучше использовать: dict.fromkeys() или множество?
В большинстве случаев лучшими вариантами являются первые два, представленные в этом руководстве:
- Преобразовать список во множество, а затем обратно.
- Создать словарь из списка с помощью
dict.fromkeys()
и вернуть его в список.
Использование словаря сохраняет порядок элементов. Однако использование множества более эффективно и может обеспечить повышение производительности, если порядок элементов не важен.
Улучшение производительности можно оценить количественно с помощью функции timeit
:
import random import timeit data = [random.randint(0, 100) for _ in range(100)] use_fromkeys = "unique_data = list(dict.fromkeys(data))" use_set = "unique_data = list(set(data))" from_keys_time = timeit.timeit(use_fromkeys, globals=globals()) set_time = timeit.timeit(use_set, globals=globals()) print(f"Time for 'dict.fromkeys()' version: {from_keys_time:0.2f}s") print(f"Time for set version: {set_time:0.2f}s")
Time for 'dict.fromkeys()' version: 2.08s Time for set version: 1.05s
Удаление дубликатов из списка с помощью множества происходит примерно в два раза быстрее, чем с помощью словаря.
Заключение
Python и его сторонние библиотеки предлагают несколько способов удалить дубликаты из списка. При выборе следует ориентироваться на нужды и особенности вашего проекта.
Самым эффективным решением будет использование множества, но при этом потеряется порядок элементов в списке. Также порядок теряется при использовании функции unique()
из библиотеки NumPy (зато функция вернет отсортированный список). Это решение уступает по эффективности использованию множества, но тоже хорошее.
Эффективными решениями, позволяющими сохранить порядок элементов, будут использование функции dict.fromkeys()
или функции unique_everseen()
из сторонней библиотеки more_itertools.
Менее эффективное решение, позволяющее сохранить порядок элементов в списке, – использование цикла.
Перевод статьи “Python: Remove Duplicates From A List (Five Solutions)”.