programing tip

순서를 유지하면서 목록에서 중복을 제거하는 방법은 무엇입니까?

itbloger 2020. 9. 30. 08:52
반응형

순서를 유지하면서 목록에서 중복을 제거하는 방법은 무엇입니까?


순서를 유지하면서 Python의 목록에서 중복을 제거하는 내장 기능이 있습니까? 세트를 사용하여 중복을 제거 할 수 있다는 것을 알고 있지만 원래 주문은 파괴됩니다. 나는 또한 다음과 같이 내 자신을 굴릴 수 있다는 것을 알고 있습니다.

def uniq(input):
  output = []
  for x in input:
    if x not in output:
      output.append(x)
  return output

( 해당 코드 샘플풀어 주셔서 감사합니다 .)

하지만 가능하다면 내장 된 관용구 나 더 파이썬적인 관용구를 이용하고 싶습니다.

관련 질문 : Python에서 순서를 유지하면서 모든 요소가 고유하도록 목록에서 중복을 제거하는 가장 빠른 알고리즘은 무엇 입니까?


여기에 몇 가지 대안이 있습니다. http://www.peterbe.com/plog/uniqifiers-benchmark

가장 빠른 것 :

def f7(seq):
    seen = set()
    seen_add = seen.add
    return [x for x in seq if not (x in seen or seen_add(x))]

왜 할당 seen.add하는 seen_add대신 호출 seen.add? Python은 동적 언어이며 seen.add각 반복을 해결하는 것은 지역 변수를 해결하는 것보다 비용이 많이 듭니다. seen.add반복 사이에 변경되었을 수 있으며 런타임은이를 배제 할만큼 똑똑하지 않습니다. 안전하게 플레이하려면 매번 개체를 확인해야합니다.

동일한 데이터 세트에서이 함수를 많이 사용할 계획이라면 http://code.activestate.com/recipes/528878/ 순서대로 설정하는 것이 좋습니다 .

O (1) 작업 별 삽입, 삭제 및 구성원 확인.

(작은 추가 참고 사항 : seen.add()항상을 반환 None하므로 or위의 내용은 논리적 테스트의 필수 부분이 아니라 집합 업데이트를 시도하는 방법 일뿐입니다.)


2016 년 수정

Raymond가 지적했듯이OrderedDict C로 구현 되는 Python 3.5+에서는 목록 이해 방식이 OrderedDict(실제로 마지막에 목록이 필요하지 않는 한-입력이 매우 짧은 경우에만) 보다 느립니다 . 따라서 3.5+에 대한 최상의 솔루션은 OrderedDict.

2015 년 중요 편집

@abarnert가 언급 했듯이 more_itertools라이브러리 ( pip install more_itertools)에는 목록 이해에서 읽을 수없는 ( ) 변형unique_everseen 없이이 문제를 해결하도록 빌드 함수가 포함되어 있습니다 . 이것은 또한 가장 빠른 솔루션입니다.not seen.add

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

하나의 간단한 라이브러리 가져 오기 및 해킹이 없습니다. 이것은 unique_everseen다음과 같은 itertools 레시피의 구현에서 비롯됩니다 .

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

파이썬에서 허용 일반적인 관용구 (작동하지만 속도, 지금 사용하는 것이 최적화되어 있지 않습니다 )이 용도 :2.7+unique_everseencollections.OrderedDict

런타임 : O (N)

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

이것은 다음보다 훨씬 좋아 보입니다.

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

추악한 해킹을 사용하지 않습니다 .

not seen.add(x)

이는 사실에 의존 set.add항상 반환에서 적절한 방법으로 None그렇게 not None평가됩니다에 True.

그러나 해킹 솔루션은 동일한 런타임 복잡성 O (N)을 갖지만 원시 속도가 더 빠릅니다.


Python 2.7 에서 원래 순서대로 유지하면서 iterable에서 중복 항목을 제거하는 새로운 방법은 다음과 같습니다.

>>> from collections import OrderedDict
>>> list(OrderedDict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']

Python 3.5 에서 OrderedDict에는 C 구현이 있습니다. 내 타이밍은 이것이 이제 Python 3.5에 대한 다양한 접근 방식 중 가장 빠르고 가장 짧다는 것을 보여줍니다.

Python 3.6 에서 일반 dict는 순서가 지정되고 압축되었습니다. (이 기능은 CPython 및 PyPy에 적용되지만 다른 구현에서는 제공되지 않을 수 있습니다.) 이는 순서를 유지하면서 새로운 가장 빠른 중복 제거 방법을 제공합니다.

>>> list(dict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']

Python 3.7 에서 일반 dict는 모든 구현에서 모두 정렬됩니다. 따라서 가장 짧고 빠른 솔루션은 다음과 같습니다.

>>> list(dict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']

@max에 대한 응답 : 3.6 또는 3.7로 이동하고 OrderedDict 대신 일반 사전을 사용하면 다른 방식으로 성능을 이길 수 없습니다. 사전은 밀도가 높고 거의 오버 헤드없이 목록으로 쉽게 변환됩니다. 대상 목록은 목록 이해에서 발생하는 모든 크기 조정을 저장하는 len (d)로 미리 크기가 조정됩니다. 또한 내부 키 목록이 조밀하기 때문에 포인터 복사가 목록 복사만큼 빠릅니다.


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

고유 → ['1', '2', '3', '6', '4', '5']


죽은 말을 걷어 차지 마십시오 (이 질문은 매우 오래되었고 이미 많은 좋은 답변이 있습니다), 여기에는 많은 상황에서 매우 빠르고 사용하기 쉬운 판다를 사용하는 솔루션이 있습니다.

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]

from itertools import groupby
[ key for key,_ in groupby(sortedList)]

목록을 정렬 할 필요조차 없습니다 . 충분한 조건은 동일한 값이 함께 그룹화된다는 것입니다.

편집 : "순서 보존"은 목록이 실제로 주문되었음을 의미한다고 가정했습니다. 그렇지 않은 경우 MizardX의 솔루션이 올바른 솔루션입니다.

커뮤니티 편집 : 그러나 이것은 "중복 된 연속 요소를 단일 요소로 압축"하는 가장 우아한 방법입니다.


질서를 유지하고 싶다면

당신은 이것을 시도 할 수 있습니다 :

list1 = ['b','c','d','b','c','a','a']    
list2 = list(set(list1))    
list2.sort(key=list1.index)    
print list2

또는 유사하게 다음을 수행 할 수 있습니다.

list1 = ['b','c','d','b','c','a','a']  
list2 = sorted(set(list1),key=list1.index)  
print list2 

다음과 같이 할 수도 있습니다.

list1 = ['b','c','d','b','c','a','a']    
list2 = []    
for i in list1:    
    if not i in list2:  
        list2.append(i)`    
print list2

다음과 같이 작성할 수도 있습니다.

list1 = ['b','c','d','b','c','a','a']    
list2 = []    
[list2.append(i) for i in list1 if not i in list2]    
print list2 

에서 파이썬 3.7 이상, 사전이되어 보장 자신의 키 삽입 순서를 기억. 질문에 대한 대답 은 현재 상황을 요약합니다.

따라서 OrderedDict솔루션은 구식이되고 import 문없이 간단히 다음과 같이 실행할 수 있습니다.

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

또 다른 아주 오래된 질문에 대한 또 다른 아주 늦은 답변 :

itertools조리법 사용하여이 작업을 수행하는 기능이 seen설정 기술을하지만, :

  • 표준 key기능을 처리 합니다.
  • 보기 흉한 해킹을 사용하지 않습니다.
  • seen.addN 번 조회하는 대신 미리 바인딩하여 루프를 최적화합니다 . ( f7이도 수행하지만 일부 버전은 그렇지 않습니다.)
  • 를 사용하여 루프를 최적화 ifilterfalse하므로 모든 요소가 아닌 Python의 고유 한 요소 만 반복하면됩니다. ( ifilterfalse물론 내부 에서 여전히 모든 것을 반복 하지만 C로되어 있으며 훨씬 빠릅니다.)

실제로보다 빠릅 f7니까? 데이터에 따라 다르므로 테스트하고 확인해야합니다. 마지막에 목록을 원하면 f7listcomp를 사용하며 여기서는 할 방법이 없습니다. ( ing append대신 직접 yield할 수 있거나 생성기를 list함수에 공급할 수 있지만 둘 다 listcomp 내부의 LIST_APPEND만큼 빠를 수는 없습니다.) 어쨌든 일반적으로 몇 마이크로 초를 짜내는 것은 다음과 같지 않을 것입니다. 장식하고 싶을 때 DSU가 필요하지 않은 쉽게 이해할 수 있고 재사용이 가능하며 이미 작성된 기능을 갖는 것이 중요합니다.

모든 레시피와 마찬가지로 more-iterools.

key사례를 원하면 다음 과 같이 단순화 할 수 있습니다.

def unique(iterable):
    seen = set()
    seen_add = seen.add
    for element in itertools.ifilterfalse(seen.__contains__, iterable):
        seen_add(element)
        yield element

그냥 외부 모듈에서 같은 기능의 다른 (매우 성능이 좋은) 구현을 추가하는 1 : iteration_utilities.unique_everseen:

>>> from iteration_utilities import unique_everseen
>>> lst = [1,1,1,2,3,2,2,2,1,3,4]

>>> list(unique_everseen(lst))
[1, 2, 3, 4]

타이밍

몇 가지 타이밍 (Python 3.6)을 수행했는데 OrderedDict.fromkeys, f7more_itertools.unique_everseen다음을 포함하여 테스트 한 다른 모든 대안보다 빠르다는 것을 보여줍니다 .

%matplotlib notebook

from iteration_utilities import unique_everseen
from collections import OrderedDict
from more_itertools import unique_everseen as mi_unique_everseen

def f7(seq):
    seen = set()
    seen_add = seen.add
    return [x for x in seq if not (x in seen or seen_add(x))]

def iteration_utilities_unique_everseen(seq):
    return list(unique_everseen(seq))

def more_itertools_unique_everseen(seq):
    return list(mi_unique_everseen(seq))

def odict(seq):
    return list(OrderedDict.fromkeys(seq))

from simple_benchmark import benchmark

b = benchmark([f7, iteration_utilities_unique_everseen, more_itertools_unique_everseen, odict],
              {2**i: list(range(2**i)) for i in range(1, 20)},
              'list size (no duplicates)')
b.plot()

여기에 이미지 설명 입력

그리고 차이가 있는지 확인하기 위해 더 많은 중복 테스트를 수행했는지 확인하기 위해 :

import random

b = benchmark([f7, iteration_utilities_unique_everseen, more_itertools_unique_everseen, odict],
              {2**i: [random.randint(0, 2**(i-1)) for _ in range(2**i)] for i in range(1, 20)},
              'list size (lots of duplicates)')
b.plot()

여기에 이미지 설명 입력

그리고 하나의 값만 포함하는 하나 :

b = benchmark([f7, iteration_utilities_unique_everseen, more_itertools_unique_everseen, odict],
              {2**i: [1]*(2**i) for i in range(1, 20)},
              'list size (only duplicates)')
b.plot()

여기에 이미지 설명 입력

이 모든 경우에 iteration_utilities.unique_everseen기능이 가장 빠릅니다 (내 컴퓨터에서).


iteration_utilities.unique_everseen함수는 입력에서 해시 할 수없는 값을 처리 할 수도 있습니다 (그러나 값이 해시 될 때 O(n*n)성능 대신 O(n)성능으로).

>>> lst = [{1}, {1}, {2}, {1}, {3}]

>>> list(unique_everseen(lst))
[{1}, {2}, {3}]

1 면책 조항 : 저는 해당 패키지의 작성자입니다.


해시 할 수없는 유형 (예 : 목록 목록)의 경우 MizardX를 기반으로합니다.

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

nub목록에 대한 Haskell의 기능을 정의하는 데 사용되는 재귀 적 아이디어를 빌리면 재귀 적 접근 방식이 될 것입니다.

def unique(lst):
    return [] if lst==[] else [lst[0]] + unique(filter(lambda x: x!= lst[0], lst[1:]))

예 :

In [118]: unique([1,5,1,1,4,3,4])
Out[118]: [1, 5, 4, 3]

데이터 크기를 늘리기 위해 시도해 보았고 하위 선형 시간 복잡성을 보았습니다 (확실한 것은 아니지만 일반 데이터에는 괜찮을 것임을 시사합니다).

In [122]: %timeit unique(np.random.randint(5, size=(1)))
10000 loops, best of 3: 25.3 us per loop

In [123]: %timeit unique(np.random.randint(5, size=(10)))
10000 loops, best of 3: 42.9 us per loop

In [124]: %timeit unique(np.random.randint(5, size=(100)))
10000 loops, best of 3: 132 us per loop

In [125]: %timeit unique(np.random.randint(5, size=(1000)))
1000 loops, best of 3: 1.05 ms per loop

In [126]: %timeit unique(np.random.randint(5, size=(10000)))
100 loops, best of 3: 11 ms per loop

나는 또한 이것이 다른 작업에 의해 고유성으로 쉽게 일반화 될 수 있다는 것도 흥미 롭다고 생각합니다. 이렇게 :

import operator
def unique(lst, cmp_op=operator.ne):
    return [] if lst==[] else [lst[0]] + unique(filter(lambda x: cmp_op(x, lst[0]), lst[1:]), cmp_op)

예를 들어 다음과 같이 고유성을 위해 "같음"인 것처럼 동일한 정수로 반올림하는 개념을 사용하는 함수를 전달할 수 있습니다.

def test_round(x,y):
    return round(x) != round(y)

그러면 unique (some_list, test_round)는 고유성이 더 이상 전통적인 평등을 의미하지 않고 (이 문제에 대한 모든 종류의 집합 기반 또는 dict-key 기반 접근 방식을 사용하여 암시 됨) 대신 가져 오기를 의미하는 목록의 고유 요소를 제공합니다. 요소가 반올림 할 수있는 가능한 각 정수 K에 대해 K로 반올림하는 첫 번째 요소 만 :

In [6]: unique([1.2, 5, 1.9, 1.1, 4.2, 3, 4.8], test_round)
Out[6]: [1.2, 5, 1.9, 4.2, 3]

5 배 더 빠른 변형 감소이지만 더 정교함

>>> l = [5, 6, 6, 1, 1, 2, 2, 3, 4]
>>> reduce(lambda r, v: v in r[1] and r or (r[0].append(v) or r[1].add(v)) or r, l, ([], set()))[0]
[5, 6, 1, 2, 3, 4]

설명:

default = (list(), set())
# use list to keep order
# use set to make lookup faster

def reducer(result, item):
    if item not in result[1]:
        result[0].append(item)
        result[1].add(item)
    return result

>>> reduce(reducer, l, default)[0]
[5, 6, 1, 2, 3, 4]

MizardX의 답변은 여러 접근 방식의 좋은 모음을 제공합니다.

이것이 내가 큰 소리로 생각하면서 생각 해낸 것입니다.

mylist = [x for i,x in enumerate(mylist) if x not in mylist[i+1:]]

'_ [1]'기호로 작성되는 목록 이해를 참조 할 수 있습니다.
예를 들어, 다음 함수는 목록 이해도를 참조하여 순서를 변경하지 않고 요소 목록을 고유 화합니다.

def unique(my_list): 
    return [x for x in my_list if x not in locals()['_[1]']]

데모:

l1 = [1, 2, 3, 4, 1, 2, 3, 4, 5]
l2 = [x for x in l1 if x not in locals()['_[1]']]
print l2

산출:

[1, 2, 3, 4, 5]

추악한 목록 이해 해킹을 할 수 있습니다.

[l[i] for i in range(len(l)) if l.index(l[i]) == i]

상대적으로 효과적인 접근 배열 :_sorted_numpy

b = np.array([1,3,3, 8, 12, 12,12])    
numpy.hstack([b[0], [x[0] for x in zip(b[1:], b[:-1]) if x[0]!=x[1]]])

출력 :

array([ 1,  3,  8, 12])

l = [1,2,2,3,3,...]
n = []
n.extend(ele for ele in l if ele not in set(n))

새 목록에 요소를 포함할지 여부를 결정하기 위해 집합의 O (1) 조회를 사용하는 생성기 식입니다.


간단한 재귀 솔루션 :

def uniquefy_list(a):
    return uniquefy_list(a[1:]) if a[0] in a[1:] else [a[0]]+uniquefy_list(a[1:]) if len(a)>1 else [a[0]]

라이너가 하나 필요하면 도움이 될 것입니다.

reduce(lambda x, y: x + y if y[0] not in x else x, map(lambda x: [x],lst))

... 작동해야하지만 내가 틀렸다면 나를 바로 잡으십시오.


을 일상적으로 사용 pandas하고 성능보다 미학이 선호되는 경우 내장 함수를 고려하십시오 pandas.Series.drop_duplicates.

    import pandas as pd
    import numpy as np

    uniquifier = lambda alist: pd.Series(alist).drop_duplicates().tolist()

    # from the chosen answer 
    def f7(seq):
        seen = set()
        seen_add = seen.add
        return [ x for x in seq if not (x in seen or seen_add(x))]

    alist = np.random.randint(low=0, high=1000, size=10000).tolist()

    print uniquifier(alist) == f7(alist)  # True

타이밍:

    In [104]: %timeit f7(alist)
    1000 loops, best of 3: 1.3 ms per loop
    In [110]: %timeit uniquifier(alist)
    100 loops, best of 3: 4.39 ms per loop

이것은 순서를 유지하고 O (n) 시간에 실행됩니다. 기본적으로 아이디어는 중복이 발견 될 때마다 구멍을 만들고 바닥으로 가라 앉히는 것입니다. 읽기 및 쓰기 포인터를 사용합니다. 중복이 발견 될 때마다 읽기 포인터 만 진행되고 쓰기 포인터는 중복 항목에 머물러 덮어 씁니다.

def deduplicate(l):
    count = {}
    (read,write) = (0,0)
    while read < len(l):
        if l[read] in count:
            read += 1
            continue
        count[l[read]] = True
        l[write] = l[read]
        read += 1
        write += 1
    return l[0:write]

가져온 모듈 또는 세트를 사용하지 않는 솔루션 :

text = "ask not what your country can do for you ask what you can do for your country"
sentence = text.split(" ")
noduplicates = [(sentence[i]) for i in range (0,len(sentence)) if sentence[i] not in sentence[:i]]
print(noduplicates)

출력을 제공합니다.

['ask', 'not', 'what', 'your', 'country', 'can', 'do', 'for', 'you']

적절한 방법

이 방법은 목록의 모든 요소에 대한 목록에 대한 선형 조회가 있기 때문에 2 차 방식입니다 ( dels로 인해 목록을 재정렬하는 비용을 추가해야 함 ).

즉, 목록의 끝에서 시작하여 왼쪽의 하위 목록에있는 각 용어를 제거하여 원점으로 진행하면 제자리에서 작동 할 수 있습니다.

코드에서이 아이디어는 간단합니다.

for i in range(len(l)-1,0,-1): 
    if l[i] in l[:i]: del l[i] 

간단한 구현 테스트

In [91]: from random import randint, seed                                                                                            
In [92]: seed('20080808') ; l = [randint(1,6) for _ in range(12)] # Beijing Olympics                                                                 
In [93]: for i in range(len(l)-1,0,-1): 
    ...:     print(l) 
    ...:     print(i, l[i], l[:i], end='') 
    ...:     if l[i] in l[:i]: 
    ...:          print( ': remove', l[i]) 
    ...:          del l[i] 
    ...:     else: 
    ...:          print() 
    ...: print(l)
[6, 5, 1, 4, 6, 1, 6, 2, 2, 4, 5, 2]
11 2 [6, 5, 1, 4, 6, 1, 6, 2, 2, 4, 5]: remove 2
[6, 5, 1, 4, 6, 1, 6, 2, 2, 4, 5]
10 5 [6, 5, 1, 4, 6, 1, 6, 2, 2, 4]: remove 5
[6, 5, 1, 4, 6, 1, 6, 2, 2, 4]
9 4 [6, 5, 1, 4, 6, 1, 6, 2, 2]: remove 4
[6, 5, 1, 4, 6, 1, 6, 2, 2]
8 2 [6, 5, 1, 4, 6, 1, 6, 2]: remove 2
[6, 5, 1, 4, 6, 1, 6, 2]
7 2 [6, 5, 1, 4, 6, 1, 6]
[6, 5, 1, 4, 6, 1, 6, 2]
6 6 [6, 5, 1, 4, 6, 1]: remove 6
[6, 5, 1, 4, 6, 1, 2]
5 1 [6, 5, 1, 4, 6]: remove 1
[6, 5, 1, 4, 6, 2]
4 6 [6, 5, 1, 4]: remove 6
[6, 5, 1, 4, 2]
3 4 [6, 5, 1]
[6, 5, 1, 4, 2]
2 1 [6, 5]
[6, 5, 1, 4, 2]
1 5 [6]
[6, 5, 1, 4, 2]

In [94]:                                                                                                                             

시퀀스에서 중복 값을 제거하지만 나머지 항목의 순서는 유지합니다. 범용 생성기 기능 사용.

# for hashable sequence
def remove_duplicates(items):
    seen = set()
    for item in items:
        if item not in seen:
            yield item
            seen.add(item)

a = [1, 5, 2, 1, 9, 1, 5, 10]
list(remove_duplicates(a))
# [1, 5, 2, 9, 10]



# for unhashable sequence
def remove_duplicates(items, key=None):
    seen = set()
    for item in items:
        val = item if key is None else key(item)
        if val not in seen:
            yield item
            seen.add(val)

a = [ {'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 1, 'y': 2}, {'x': 2, 'y': 4}]
list(remove_duplicates(a, key=lambda d: (d['x'],d['y'])))
# [{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}]

zmk의 접근 방식은 매우 빠르지 만 순서를 자연스럽게 유지하는 목록 이해를 사용합니다. 대소 문자를 구분하는 문자열에 적용하기 위해 쉽게 수정할 수 있습니다. 이것은 또한 원래 케이스를 보존합니다.

def DelDupes(aseq) :
    seen = set()
    return [x for x in aseq if (x.lower() not in seen) and (not seen.add(x.lower()))]

밀접하게 관련된 기능은 다음과 같습니다.

def HasDupes(aseq) :
    s = set()
    return any(((x.lower() in s) or s.add(x.lower())) for x in aseq)

def GetDupes(aseq) :
    s = set()
    return set(x for x in aseq if ((x.lower() in s) or s.add(x.lower())))

참고 URL : https://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-whilst-preserving-order

반응형