화면에 표시할 태그에서 중복만 지우려고 list(set(tags))를 썼다. 중복은 사라졌는데, 사용자가 고른 순서까지 달라졌다. 태그 목록이 갑자기 '섞기 재생'을 시작한 것처럼 보이는 이유는 집합이 순서를 보관하는 자료형이 아니기 때문이다.
같은 태그 여섯 개로 중복 제거와 순서 보존을 따로 확인해 보자.
list(set())은 왜 원래 순서를 유지하지 않을까?
tags = ["db", "api", "web", "db", "linux", "api"]
unique = list(set(tags))
print(unique) # 원소 네 개; 순서는 보장되지 않음set(tags)는 같은 값을 하나로 모으지만, 각 값이 처음 몇 번째에 나왔는지는 기록하지 않는다. 뒤의 list()는 집합을 리스트로 바꿀 뿐, 사라진 순서 정보를 복구하지 못한다. 내 컴퓨터에서 우연히 db가 먼저 보여도 그 순서에 기대면 안 된다.
파이썬 문서도 집합을 순서 없는 서로 다른 값의 모음으로 정의한다. 집합이 중복값을 제거한다는 사실과 결과를 오름차순으로 정렬한다는 주장은 다르다.
처음 등장한 순서대로 중복을 지우려면?
dict.fromkeys(tags)는 값을 키로 한 번씩 넣는다. 이미 있는 키가 다시 나와도 앞서 들어간 위치는 바뀌지 않는다. 파이썬 3.7 이상에서 딕셔너리는 삽입 순서를 보존하므로, 키를 리스트로 만들면 첫 등장 순서가 남는다.
도식의 오른쪽 결과는 첫 번째 db와 api의 위치를 유지한다. 같은 태그 목록을 dict.fromkeys에 넣어 중복 제거 뒤 순서를 확인해 보자.
tags = ["db", "api", "web", "db", "linux", "api"]
unique = list(dict.fromkeys(tags))
print(unique) # ['db', 'api', 'web', 'linux']
print(tags) # 원본은 그대로확인할 것은 두 가지다. 중복된 db와 api가 한 번씩만 남았는가? 남은 값의 차례가 첫 입력 순서와 같은가? 이 예제는 둘 다 만족한다.
정렬된 결과가 필요하다면 sorted(set())일까?
입력 순서가 아니라 알파벳순이 요구사항이면 그때는 정렬을 명시적으로 한다.
tags = ["db", "api", "web", "db", "linux", "api"]
print(sorted(set(tags))) # ['api', 'db', 'linux', 'web']sorted()가 정렬을 보장하는 것이지, set()이 정렬하는 것은 아니다. 화면의 사용자가 고른 순서를 지켜야 하는지, 사전순 목록을 만들어야 하는지부터 정하면 코드도 간단해진다.
리스트나 딕셔너리를 넣었는데 TypeError가 난다면?
set()과 dict.fromkeys()는 모두 원소를 해시 가능한 값으로 다룬다. 원소 자체가 리스트나 딕셔너리면 TypeError: unhashable type이 난다. 이때는 전체 객체를 그대로 넣지 말고, 중복 판단에 쓸 안정적인 키를 먼저 정해야 한다. 예를 들어 객체 목록이라면 ID를 기준으로 첫 항목만 남길지, 내용이 같은 항목을 하나로 볼지부터 결정한다.
핵심 요약
list(set(tags))는 중복을 없애지만 입력 순서는 보장하지 않는다. 처음 나온 순서가 중요하면 list(dict.fromkeys(tags)), 정렬이 목적이면 sorted(set(tags))를 사용한다. 중복 제거와 순서 결정은 같은 작업처럼 보여도 별개의 요구사항이다.

