
이미지 출처: Pexels · Dreamcatchlight · Pexels License
"리스트를 만들 때 대괄호 대신 소괄호를 썼더니 값을 고치는 줄에서 에러가 납니다. 괄호 모양만 바꿨는데 왜 그런가요?"
지난 편 딕셔너리 글에 달린 질문입니다. 괄호를 바꾸면 자료형이 바뀝니다. 대괄호는 리스트, 소괄호는 튜플, 중괄호에 값만 넣으면 집합입니다. 셋은 생긴 것보다 하는 일이 더 다릅니다.
학기 초에 받은 명단과, 출석을 부른 결과를 놓고 시작하겠습니다.
members = ["홍길동", "김철수", "이영희"]
checked = ["김철수", "홍길동", "김철수", "이영희", "홍길동"]
둘 다 리스트인데 성격이 다릅니다. 위는 학기 내내 바뀌면 안 되는 명단이고, 아래는 같은 이름이 겹쳐 있어서 그대로 세면 다섯 명이 됩니다. 파이썬에는 이 두 일에 맞는 그릇이 따로 있습니다.
members = ("홍길동", "김철수", "이영희")
checked = {"김철수", "홍길동", "김철수", "이영희", "홍길동"}
print(len(members), len(checked))
3 3
위는 고칠 수 없게 잠겼고, 아래는 겹친 이름이 사라져 세 명이 됐습니다. 질문 여덟 개로 나눠 답하겠습니다.
튜플이 뭔가요 — 리스트와 무엇이 다른가요
파이썬 공식 자습서는 튜플을 쉼표로 구분된 여러 값을 하나로 묶은, 바꿀 수 없는 순서열이라고 설명합니다. 리스트와 같은 점부터 적으면, 순서가 있고 0번부터 번호로 꺼냅니다. 다른 점은 하나입니다. 만든 뒤에 내용을 바꿀 수 없습니다.
| 성질 | 리스트 [ ] | 튜플 ( ) |
|---|---|---|
| 순서·번호로 꺼내기 | 있음 | 있음 |
| 만든 뒤 고치기 | append·remove·x[0] = ... 전부 가능 | 전부 불가 |
| 딕셔너리의 키로 | 불가 | 가능 |
| 쓰임 | 늘고 줄고 바뀌는 목록 | 한 번 정하면 끝인 묶음 |
이 편에서 쓰는 말은 넷입니다.
| 용어 | 뜻 |
|---|---|
| 튜플(tuple) | 소괄호와 쉼표로 묶은, 바꿀 수 없는 순서열 |
| 불변(immutable) | 만든 뒤 내용을 바꿀 수 없다는 성질. 지난 1편의 문자열도 불변입니다 |
| 언패킹(unpacking) | 묶음 안의 값을 변수 여러 개에 한 번에 나눠 담는 것 |
| 집합(set) | 순서도 없고 중복도 없는 값의 모임. 중괄호 { }에 값만 넣습니다 |
비유하자면 리스트는 뚜껑이 열리는 상자이고 튜플은 봉인을 찍은 봉투입니다. 안에 든 것은 같아도 봉투는 뜯지 않는 이상 내용을 바꿀 수 없고, 바꾸려면 새 봉투를 만들어야 합니다.
괄호만 바꿨는데 왜 고칠 수 없나요
직접 고쳐 보겠습니다.
members = ["홍길동", "김철수", "이영희"]
members[0] = "박민수"
print(members)
members = ("홍길동", "김철수", "이영희")
members[0] = "박민수"
print(members)📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
['박민수', '김철수', '이영희']
TypeError: 'tuple' object does not support item assignment
첫 번째는 바뀌었고, 두 번째는 멈췄습니다. 오류 이름이 TypeError인 것이 힌트입니다. 값이 잘못된 게 아니라 그 자료형이 그 동작을 지원하지 않는다는 뜻입니다. item assignment는 칸에 값을 넣는 일이고, 튜플에는 그 기능 자체가 없습니다.

바뀐 것은 괄호 두 개뿐인데, 아래쪽은 봉인이 찍혀 칸을 고칠 수 없습니다.
append나 remove도 없습니다. 리스트에서 쓰던 메서드를 튜플에 쓰면 AttributeError: 'tuple' object has no attribute 'append'가 납니다. 없는 기능을 부른 것입니다.
한 가지 선을 그어 두겠습니다. 봉인은 봉투에 찍힌 것이지 안에 든 물건에 찍힌 것이 아닙니다. 튜플 안에 리스트가 들어 있으면 그 리스트는 여전히 바뀝니다.
t = (1, [2, 3])
t[1].append(4)
print(t)
(1, [2, 3, 4])
t[1]을 다른 것으로 바꾸는 일은 막히지만, t[1]이 가리키는 리스트에 값을 더하는 일은 막히지 않습니다. 튜플이 지키는 것은 "어느 칸에 무엇이 들어 있는가"이지 그 무엇의 속까지는 아닙니다.
고칠 수 없는데 어디에 쓰나요
고칠 수 없다는 것은 단점처럼 들리지만, 실제로는 바뀌면 안 되는 값을 바뀌지 않게 만드는 것이 튜플의 일입니다. 세 자리에서 씁니다.
첫째, 값 여러 개를 한 번에 나눠 받을 때입니다. 이걸 언패킹이라고 부릅니다.
def min_max(nums):
return min(nums), max(nums)
low, high = min_max([7, 2, 9, 4])
print(low, high)
x, y = (10, 20)
print(x, y)
x, y = y, x
print(x, y)📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
return min(nums), max(nums)는 쉼표로 값 두 개를 묶어 튜플 하나로 돌려줍니다. 받는 쪽에서 low, high =로 쓰면 그 튜플이 두 변수에 나뉘어 들어갑니다. 마지막 줄 x, y = y, x는 임시 변수 없이 두 값을 맞바꾸는 파이썬식 표현입니다. 오른쪽이 먼저 튜플로 묶이고 왼쪽에 풀립니다.
둘째, 딕셔너리의 키로 쓸 때입니다. 지난 편에서 리스트는 키가 될 수 없다고 했습니다. 내용이 바뀌니까요. 튜플은 바뀌지 않으니 됩니다.
seats = {(1, 3): "홍길동", (2, 5): "김철수"}
print(seats[(1, 3)])
try:
bad = {[1, 3]: "홍길동"}
except TypeError as e:
print("리스트는", e)📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
홍길동
리스트는 unhashable type: 'list'
1행 3열 자리에 누가 앉았는지를 찾을 때 (1, 3) 두 값이 한 몸으로 키가 됩니다. 리스트로 같은 일을 하면 unhashable이라는 낯선 말과 함께 막힙니다. 바뀔 수 있는 것은 이름표로 쓸 수 없다는 뜻입니다.
셋째, 함수에 넘긴 값이 안에서 바뀌지 않았으면 할 때입니다. 리스트를 넘기면 함수 안에서 append 한 번에 원본이 변합니다. 튜플을 넘기면 그런 일이 생기지 않습니다. 저는 이 세 번째가 가장 실용적인 이유라고 봅니다. 고칠 수 없다는 성질이 "실수로 고쳐질 일이 없다"는 보장으로 바뀌는 자리입니다.
(1,)에 쉼표는 왜 붙나요
여기가 이 편에서 가장 작은 함정입니다. 원소 하나짜리 튜플을 만들려고 (1)이라고 쓰면 튜플이 되지 않습니다.
a = (1)
b = (1,)
c = 1,
print(type(a))
print(type(b))
print(type(c))📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
<class 'int'>
<class 'tuple'>
<class 'tuple'>
(1)은 그냥 숫자 1입니다. (2 + 3) * 4의 괄호와 같은 괄호라서, 계산 순서만 정하고 사라집니다. 튜플을 만드는 것은 괄호가 아니라 쉼표입니다. 그래서 1,처럼 괄호 없이 쉼표만 찍어도 튜플이 되고, (1,)처럼 괄호 안에 쉼표를 찍어야 원소 하나짜리 튜플이 됩니다.

괄호가 아니라 쉼표가 튜플을 만듭니다. 원소 하나면 쉼표를 끝에 찍습니다.
원소가 둘 이상이면 쉼표가 저절로 들어가니 신경 쓸 일이 없습니다. 하나일 때만 이 일이 생깁니다. 함수에 튜플 하나를 넘기려다 숫자를 넘기고, 뒤에서 len()이 TypeError: object of type 'int' has no len()으로 터지는 것이 전형적인 모양입니다.
튜플에 값을 더하고 싶으면 어떻게 하느냐는 질문이 여기서 따라옵니다. 고칠 수 없으니 append는 없습니다. 대신 새 튜플을 만듭니다.
t = (1,)
t = t + (2,)
print(t)
(1, 2)
t + (2,)는 원래 튜플에 값을 넣는 게 아니라 두 튜플을 이어 붙인 새 튜플을 만들고, 그것을 t라는 이름에 다시 붙입니다. 1편에서 s = s.strip()으로 결과를 받아야 했던 것과 같은 모양입니다. 원본은 그대로이고 이름이 새것을 가리킵니다.
집합은 뭔가요 — 왜 중복을 버리나요
공식 자습서의 정의는 순서가 없고 중복 원소가 없는 컬렉션입니다. 앞의 두 낱말이 곧 성질입니다.
| 성질 | 뜻 |
|---|---|
| 중복이 없다 | 같은 값을 두 번 넣어도 하나만 남습니다 |
| 순서가 없다 | 몇 번째라는 개념이 없어 s[0]을 못 씁니다 |
| 있는지 묻기가 빠르다 | x in s가 리스트보다 훨씬 빠릅니다 |
| 바뀌지 않는 값만 담는다 | 리스트는 넣을 수 없고 튜플은 됩니다 |
중괄호에 값만 넣으면 집합입니다. 지난 편의 딕셔너리는 중괄호에 키: 값 쌍을 넣었습니다. 기호가 같아서 한 군데가 헷갈립니다.
a = {1, 2, 3}
b = {"a": 1}
c = {}
d = set()
print(type(a))
print(type(b))
print(type(c))
print(type(d))
print(set("hello"))📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
<class 'set'>
<class 'dict'>
<class 'dict'>
<class 'set'>
{'h', 'e', 'l', 'o'}
빈 중괄호 {}는 빈 딕셔너리입니다. 빈 집합은 set()으로만 만듭니다. 이건 문법이 그렇게 정해진 것이라 외우는 수밖에 없습니다. 마지막 줄 set("hello")에서 l이 하나만 남은 것이 중복을 버린다는 뜻이고, 순서가 h e l l o와 다른 것이 순서가 없다는 뜻입니다. 출력 순서는 실행할 때마다 다를 수 있습니다.
순서가 없으니 번호로는 못 꺼냅니다.
s = {1, 2, 3}
print(s[0])
TypeError: 'set' object is not subscriptable
subscriptable은 대괄호로 꺼낼 수 있다는 뜻입니다. 집합은 그게 안 됩니다. 집합은 꺼내는 그릇이 아니라 있는지 묻고, 겹치는 것을 걸러내는 그릇입니다.
중복 제거에 왜 집합이 가장 짧나요
출석 결과로 돌아가겠습니다. 같은 이름이 겹쳐 찍힌 리스트에서 실제 몇 명이 왔는지 세는 일입니다.
집합 없이 하면 이렇게 됩니다.
checked = ["김철수", "홍길동", "김철수", "이영희", "홍길동"]
unique = []
for name in checked:
if name not in unique:
unique.append(name)
print(len(unique))
다섯 줄입니다. 틀린 코드는 아닙니다. 다만 "겹치지 않게 모은다"는 일 자체가 집합의 정의라서, 집합에 넣기만 하면 끝납니다.
checked = ["김철수", "홍길동", "김철수", "이영희", "홍길동"]
print(len(checked))
print(len(set(checked)))
print(set(checked))📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
5
3
{'홍길동', '김철수', '이영희'}
set(checked)는 리스트를 집합으로 바꾸고, 그 과정에서 겹친 이름이 사라집니다. 다섯 줄이 한 줄이 된 이유는 요령이 아니라 자료형이 이미 그 규칙을 갖고 있기 때문입니다.

집합을 거치면 겹침은 사라지지만 순서도 사라집니다. 순서가 필요하면 뒤에 정렬하거나, 처음 순서를 지키는 다른 길로 갑니다.
여기에 함정이 하나 딸려 옵니다. 순서가 사라집니다. 명단이 원래 김철수·홍길동·이영희 순서였다는 사실은 집합을 거치는 순간 없어집니다. 순서가 필요하면 두 가지 길이 있습니다.
checked = ["김철수", "홍길동", "김철수", "이영희", "홍길동"]
print(sorted(set(checked)))
print(list(dict.fromkeys(checked)))
['김철수', '이영희', '홍길동']
['김철수', '홍길동', '이영희']
sorted(set(...))는 겹침을 없앤 뒤 가나다순으로 새 리스트를 만듭니다. dict.fromkeys(...)는 리스트의 값을 딕셔너리 키로 삼는데, 키는 겹칠 수 없고 넣은 순서를 기억하니 처음 나온 순서를 지킨 채 겹침만 빠집니다. 정렬이 필요하면 앞엣것, 원래 순서가 필요하면 뒤엣것입니다.
집합끼리 계산이 되나요
됩니다. 이름 그대로 수학의 집합 연산입니다. 1반과 2반의 동아리 명단으로 보겠습니다.
a = {"홍길동", "김철수", "이영희"}
b = {"김철수", "박민수"}
print(a & b)
print(a | b)
print(a - b)
a.add("최지우")
a.add("홍길동")
print(len(a))
print(sorted(a))📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
{'김철수'}
{'홍길동', '김철수', '이영희', '박민수'}
{'홍길동', '이영희'}
4
['김철수', '이영희', '최지우', '홍길동']
a & b— 양쪽에 다 있는 사람. 교집합입니다.a | b— 어느 한쪽이라도 있는 사람. 합집합이고, 김철수는 한 번만 셉니다.a - b— a에는 있고 b에는 없는 사람. 차집합입니다.add()— 하나 넣습니다. 이미 있는 홍길동을 다시 넣어도 개수는 그대로입니다.sorted(a)— 집합에는 순서가 없으니 정렬하려면sorted()로 리스트를 새로 만듭니다.a.sort()는 없습니다.
x in a로 있는지 묻는 것도 됩니다. 리스트에서 in은 앞에서부터 하나씩 견주지만 집합은 바로 찾아갑니다. 명단이 수만 명이면 이 차이가 체감됩니다.
그래서 언제 무엇을 쓰나요
이제 그릇이 넷입니다. 지난 편의 리스트·딕셔너리에 이 편의 튜플·집합을 더해 한 표에 놓겠습니다.
| 그릇 | 기호 | 순서 | 고치기 | 중복 | 이런 일에 |
|---|---|---|---|---|---|
| 리스트 | [1, 2] | 있음 | 됨 | 됨 | 늘고 줄고 바뀌는 목록 |
| 튜플 | (1, 2) | 있음 | 안 됨 | 됨 | 한 번 정하면 끝인 묶음, 딕셔너리 키 |
| 집합 | {1, 2} | 없음 | 됨 | 안 됨 | 있는지 묻기, 겹침 걸러내기 |
| 딕셔너리 | {"a": 1} | 넣은 순서 | 됨 | 키는 안 됨 | 이름표로 값 찾기 |
고르는 기준은 두 질문이면 됩니다. 바뀌어야 하는가와 겹쳐도 되는가입니다.
- 바뀌면 안 되면 튜플입니다. 좌표, 날짜의 (년, 월, 일), 함수가 돌려주는 값 여러 개.
- 겹치면 안 되면 집합입니다. 방문자 목록, 사용한 아이디, 두 명단의 겹침.
- 둘 다 아니면 리스트이고, 이름으로 찾아야 하면 딕셔너리입니다.
애매하면 리스트로 시작해도 됩니다. 다만 코드 어딘가에 if x not in unique:가 보이면 그 자리는 집합이 할 일이고, "이 값은 절대 바뀌면 안 되는데"라는 주석이 보이면 그 자리는 튜플이 할 일입니다.
한 글자 바꾸면 무엇이 달라질까요
미니 퀴즈
참고 자료
- 파이썬 공식 자습서 5.3 "튜플과 시퀀스" — 튜플의 정의, 원소 하나짜리 튜플의 쉼표, 언패킹 https://docs.python.org/ko/3/tutorial/datastructures.html
- 파이썬 공식 자습서 5.4 "집합" — 정의, 빈 집합은 set(), 집합 연산 https://docs.python.org/ko/3/tutorial/datastructures.html
- 파이썬 표준 라이브러리 "집합 형 — set, frozenset" — add·remove·연산자 전체 https://docs.python.org/ko/3/library/stdtypes.html
포켓코딩(PYTHON2) 3편 · 2편(파이썬 딕셔너리)에서 이어집니다. 예제 코드는 강사 개인 GitHub 저장소의 강의 자료를 바탕으로 다시 썼습니다. 다음 편은 리스트 컴프리헨션입니다. for문 네 줄을 대괄호 한 줄로 줄이는 문법과, 줄이다가 오히려 안 읽히게 되는 지점을 다룹니다.