Получение первых n уникальных элементов из списка Python
У меня есть список Python, где элементы могут повторяться.
>>> a = [1,2,2,3,3,4,5,6]
Я хочу получить первые n
уникальных элементов из списка. Итак, в этом случае, если я захочу первые 5 уникальных элементов, они будут:
[1,2,3,4,5]
Я пришел к решению с использованием генераторов:
def iterate(itr, upper=5):
count = 0
for index, element in enumerate(itr):
if index==0:
count += 1
yield element
elif element not in itr[:index] and count<upper:
count += 1
yield element
В использовании:
>>> i = iterate(a, 5)
>>> [e for e in i]
[1,2,3,4,5]
У меня есть сомнения в том, что это самое оптимальное решение. Есть ли альтернативная стратегия, которую я могу реализовать, чтобы написать ее более питонным и эффективным способом?
Ответы
Ответ 1
Я бы использовал set
для запоминания увиденного и возврата из генератора, когда вы seen
достаточно:
a = [1,2,2,3,3,4,5,6]
def get_unique_N(iterable, N):
"""Yields (in order) the first N unique elements of iterable.
Might yield less if data too short."""
seen = set()
for e in iterable:
if e in seen:
continue
seen.add(e)
yield e
if len(seen) == N:
return
k = get_unique_N([1,2,2,3,3,4,5,6], 4)
print(list(k))
Выход:
[1,2,3,4]
Согласно PEP-479, вы должны return
из генераторов, а не raise StopIteration
- спасибо @khelwood & @iBug за этот комментарий - никто никогда не узнает об этом.
С 3.6 вы получаете устаревшее предупреждение, с 3.7 выдает RuntimeErrors: Transition Plan, если все еще используете raise StopIteration
Ваше решение, использующее elif element not in itr[:index] and count<upper:
использует O(k)
поисков - где k
- длина фрагмента - использование набора сокращает это до O(1)
поисков, но использует больше памяти, потому что сет должен быть сохранен. Это компромисс между скоростью и памятью - что лучше, зависит от приложения/данных.
Рассмотрим [1,2,3,4,4,4,4,5]
против [1]*1000+[2]*1000+[3]*1000+[4]*1000+[5]*1000+[6]
:
Для 6 уникальных (более длинный список):
- у вас будет поиск
O(1)+O(2)+...+O(5001)
- у меня будет
5001*O(1)
поиск + память для set( {1,2,3,4,5,6})
Ответ 2
Вы можете адаптировать популярный рецепт itertools
unique_everseen
:
def unique_everseen_limit(iterable, limit=5):
seen = set()
seen_add = seen.add
for element in iterable:
if element not in seen:
seen_add(element)
yield element
if len(seen) == limit:
break
a = [1,2,2,3,3,4,5,6]
res = list(unique_everseen_limit(a)) # [1, 2, 3, 4, 5]
В качестве альтернативы, как предлагает @Chris_Rands, вы можете использовать itertools.islice
для извлечения фиксированного числа значений из неограниченного генератора:
from itertools import islice
def unique_everseen(iterable):
seen = set()
seen_add = seen.add
for element in iterable:
if element not in seen:
seen_add(element)
yield element
res = list(islice(unique_everseen(a), 5)) # [1, 2, 3, 4, 5]
Обратите внимание, что рецепт unique_everseen
доступен в сторонних библиотеках через more_itertools.unique_everseen
или toolz.unique
, так что вы можете использовать:
from itertools import islice
from more_itertools import unique_everseen
from toolz import unique
res = list(islice(unique_everseen(a), 5)) # [1, 2, 3, 4, 5]
res = list(islice(unique(a), 5)) # [1, 2, 3, 4, 5]
Ответ 3
Если ваши объекты hashable (int
является hashable), вы можете написать функцию полезности, используя fromkeys
метод из collections.OrderedDict
класса (или начиная с Python3.7 простым dict
, так как они стали официально заказаны), как
from collections import OrderedDict
def nub(iterable):
"""Returns unique elements preserving order."""
return OrderedDict.fromkeys(iterable).keys()
и тогда реализация iterate
может быть упрощена до
from itertools import islice
def iterate(itr, upper=5):
return islice(nub(itr), upper)
или если вы хотите всегда list
в качестве вывода
def iterate(itr, upper=5):
return list(nub(itr))[:upper]
улучшения
Как упомянул @Chris_Rands, это решение проходит через всю коллекцию, и мы можем улучшить это, написав утилиту nub
в форме генератора, как уже делали другие:
def nub(iterable):
seen = set()
add_seen = seen.add
for element in iterable:
if element in seen:
continue
yield element
add_seen(element)
Ответ 4
Вы можете использовать OrderedDict
или, начиная с Python 3.7, обычный dict
, так как они реализованы для сохранения порядка вставки. Обратите внимание, что это не будет работать с наборами.
N = 3
a = [1, 2, 2, 3, 3, 3, 4]
d = {x: True for x in a}
list(d.keys())[:N]
Ответ 5
Вот Pythonic подход с использованием itertools.takewhile()
:
In [95]: from itertools import takewhile
In [96]: seen = set()
In [97]: set(takewhile(lambda x: seen.add(x) or len(seen) <= 4, a))
Out[97]: {1, 2, 3, 4}
Ответ 6
Есть действительно удивительные ответы на этот вопрос, которые быстрые, компактные и блестящие! Причина, по которой я привожу здесь этот код, состоит в том, что я считаю, что существует множество случаев, когда вам не нужно терять 1 микросекунду, или вам не нужны дополнительные библиотеки в вашем коде для единовременного решения простой задачи.
a = [1,2,2,3,3,4,5,6]
res = []
for x in a:
if x not in res: # yes, not optimal, but doesnt need additional dict
res.append(x)
if len(res) == 5:
break
print(res)
Ответ 7
Использование set
с sorted+ key
sorted(set(a), key=list(a).index)[:5]
Out[136]: [1, 2, 3, 4, 5]
Ответ 8
Если предположить, что элементы упорядочены, как показано, это возможность весело провести время с groupby
функции в itertools:
from itertools import groupby, islice
def first_unique(data, upper):
return islice((key for (key, _) in groupby(data)), 0, upper)
a = [1, 2, 2, 3, 3, 4, 5, 6]
print(list(first_unique(a, 5)))
Обновлен для использования islice
вместо enumerate
per @juanpa.arrivillaga. Вам даже не нужен set
для отслеживания дубликатов.
Ответ 9
Дано
import itertools as it
a = [1, 2, 2, 3, 3, 4, 5, 6]
Код
Простое понимание списка (похоже на ответ @cdlane).
[k for k, _ in it.groupby(a)][:5]
# [1, 2, 3, 4, 5]
В качестве альтернативы в Python 3. 6+:
list(dict.fromkeys(a))[:5]
# [1, 2, 3, 4, 5]
Ответ 10
Почему бы не использовать что-то подобное?
>>> a = [1, 2, 2, 3, 3, 4, 5, 6]
>>> list(set(a))[:5]
[1, 2, 3, 4, 5]
Ответ 11
Вы можете попробовать метод, который использует наборы. В sets
Python возвращает неупорядоченные коллекции уникальных элементов. Следующий код дает пример того, как использовать se
в вашей проблеме.
a = [1,2,2,3,3,4,5,6]
b = []
flag = True
while(flag):
num = 5
b = list(set(a))[:num]
if(len(b)==5): flag = False
print b
>>>[1 2 3 4 5]
Ответ 12
a = [1,2,2,3,3,4,5,6]
def unique(a,size):
return list(set(a))[:size]
print(unique(a,5))
Ответ 13
Пример списка:
a = [1, 2, 2, 3, 3, 4, 5, 6]
Функция возвращает все или количество уникальных предметов, необходимых из списка
1-й аргумент - список для работы, 2-й аргумент (необязательно) - количество уникальных элементов (по умолчанию - Нет - это означает, что будут возвращены все уникальные элементы)
def unique_elements(lst, number_of_elements=None):
return list(dict.fromkeys(lst))[:number_of_elements]
Вот пример, как это работает. Имя списка - "а", и нам нужно получить 2 уникальных элемента:
print(unique_elements(a, 2))
Выход:
Ответ 14
a = [1,2,2,3,3,4,5,6]
from collections import defaultdict
def function(lis,n):
dic = defaultdict(int)
sol=set()
for i in lis:
try:
if dic[i]:
pass
else:
sol.add(i)
dic[i]=1
if len(sol)>=n:
break
except KeyError:
pass
return list(sol)
print(function(a,3))
выход
[1, 2, 3]
Ответ 15
Сделай это:
sorted(set(a), key=a)[:5]
Это работает, потому что наборы не повторяются.
Ответ 16
Вы используете набор, чтобы получить первые n уникальных значений, но он не сохраняет порядок внутри первых n
уникальных значений.
n=5
s=set()
for i in a:
if len(s)<n: s.add(i)
else: break
print(s)