Python 강좌 : 제 11강 - Tuple 기초 연산
튜플(Tuple)
Python에서는 소괄호(())를 이용하여 튜플을 생성할 수 있습니다.
튜플은 생성한 뒤에는 요소를 변경할 수 없는(Immutable) 자료형이며, 목록과 마찬가지로 순서를 중요시합니다.
주로, 변하지 않는 값이나 위치, 크기 등 한 쌍을 이루는 객체들을 표현할 때 사용합니다.
변경할 수 없으므로 사전(Dictionary)의 key나 집합(Set)의 원소로도 사용할 수 있습니다. (단, 튜플 안의 요소도 모두 변경할 수 없는 값이어야 합니다.)
튜플은 한마디로 “변경할 수 없는 목록”입니다. 원소를 순서대로 담고 인덱싱·슬라이싱으로 꺼내는 방법은 제 8강의 목록과 같지만, 한 번 만든 뒤에는 원소를 바꾸거나 추가·삭제할 수 없습니다.
제 8강에서 사용한 네 가지 기준으로 튜플을 정리하면 다음과 같습니다.
- 변경 불가능(Immutable) : 원소를 바꾸거나 추가·삭제할 수 없습니다.
- 순서가 있음 : 원소는
0번부터 시작하는 위치를 가집니다. - 중복 허용 : 같은 값이 여러 번 들어 있어도 됩니다.
- 해시 가능(Hashable) : 모든 원소가 해시 가능하면 튜플도 해시 가능하여, 사전의
key나 집합의 원소가 될 수 있습니다.
기능이 더 적은 튜플을 따로 사용하는 이유는 바뀌지 않는다는 보장 자체가 쓸모 있기 때문입니다.
(위도, 경도),(너비, 높이),(빨강, 초록, 파랑)처럼 정해진 개수의 값이 한 묶음으로 의미를 가지는 데이터를 실수로 바꾸지 못하게 보호합니다.- 내용이 바뀌지 않으므로 해시값도 바뀌지 않아,
{(37, 127): "서울"}처럼 좌표를 사전의 key로 사용할 수 있습니다. - 함수가 여러 값을 한 번에 반환할 때 Python은 자동으로 튜플로 묶어 돌려줍니다.
이번 강좌에서는 튜플의 생성, 참조, 불변성, 패킹과 언패킹, 조사를 다루고, 마지막으로 목록과 튜플을 언제 선택하는지 정리합니다.
생성
튜플은 값을 쉼표(,)로 구분하여 소괄호(())로 감싸서 만듭니다. 목록의 대괄호를 소괄호로 바꾼 모양이지만, 원소가 하나뿐일 때는 주의할 점이 있습니다.
a = (1, 2, 3, 4, 5)
b = (6,)
c = tuple("123")
print(a)
print(b)
print(c)- 결과
- (1, 2, 3, 4, 5)
(6,)
(‘1’, ‘2’, ‘3’)
소괄호(())를 이용하여 목록(List)과 동일한 방법으로 튜플을 생성할 수 있습니다.
요소를 하나만 갖는 튜플을 생성할 경우, (6,)처럼 요소 뒤에 쉼표를 추가해 튜플로 표현할 수 있습니다.
또한, tuple()을 통해서도 다른 형식의 데이터를 튜플로 변경할 수 있습니다.
tuple("123")은 문자열에서 문자를 하나씩 꺼내 튜플로 만들므로, 숫자 123이 아니라 문자 '1', '2', '3'을 원소로 가집니다. 출력에서도 원소가 하나인 튜플은 (6,)처럼 쉼표를 붙여 표시하여 일반 괄호와 구분합니다.
a = (6)
b = (6,)
c = 3, 4
print(type(a))
print(type(b))
print(c)- 결과
- <class ‘int’>
<class ‘tuple’>
(3, 4)
쉼표 없이 (6)으로 작성하면 소괄호는 단순히 연산 순서를 묶는 괄호로 해석되어 정수가 됩니다.
튜플을 만드는 것은 소괄호가 아니라 쉼표이므로, 3, 4처럼 소괄호 없이 작성해도 튜플이 생성됩니다.
(1 + 2) * 3의 괄호처럼 소괄호는 계산 순서를 정할 때도 쓰이므로, Python은 쉼표가 없는 (6)을 그냥 6으로 해석합니다. 반대로 소괄호가 없어도 쉼표가 있으면 튜플이 됩니다. 다만 코드를 읽기 쉽도록 보통은 소괄호를 함께 씁니다.
a = ()
b = tuple([1, 2, 3])
print(a, len(a))
print(b)
print(tuple(range(3)))- 결과
- () 0
(1, 2, 3)
(0, 1, 2)
빈 튜플은 원소 없이 ()로 만들며 길이는 0입니다. 쉼표가 필요한 것은 원소가 하나인 경우뿐입니다.
tuple()에 목록이나 range()처럼 원소를 꺼낼 수 있는 객체를 전달하면, 같은 원소를 같은 순서로 담은 튜플이 만들어집니다. 원본 목록은 그대로 남습니다.
참조
튜플은 순서가 있는 시퀀스이므로, 목록과 완전히 같은 문법으로 원소를 꺼낼 수 있습니다. 튜플이 막는 것은 “변경”뿐이며, “읽기”는 모두 허용됩니다.
a = (1, 2, 3, 4, 5)
print(a[0])
print(a[1])
print(a[2])
print(a[-1])
print(a[0:2])
print(a[0:-1])
print(a[0:-1:2])- 결과
- 1
2
3
5
(1, 2)
(1, 2, 3, 4)
(1, 3)
튜플은 목록과 마찬가지로 대괄호([])와 :을 이용하여 튜플을 참조할 수 있습니다.
튜플[index]를 사용하면 index의 값을 출력합니다.
튜플[start:end]를 사용하면 start부터 end-1까지 출력합니다.
튜플[start:end:step]를 사용하면 start부터 end-1까지 step 간격만큼 출력합니다.
첫 번째 값은 0이며, 우측의 첫 번째 값은 -1을 사용하여 출력이 가능합니다.
결과를 읽으면, a[0:2]는 0번과 1번 원소 (1, 2), a[0:-1]은 마지막 원소를 뺀 (1, 2, 3, 4), a[0:-1:2]는 그중 두 칸 간격인 (1, 3)입니다. 인덱싱 결과는 원소 자체(1)이고, 슬라이싱 결과는 튜플((1, 2))입니다.
- Tip : 튜플을 슬라이싱한 결과도 튜플입니다.
불변성
튜플을 쓰는 가장 큰 이유는 변경할 수 없다는 성질입니다. 이 절에서는 변경을 시도하면 어떻게 되는지, 그리고 “변경할 수 없다”가 정확히 무엇을 의미하는지 확인합니다.
a = (1, 2, 3)
try:
a[0] = 10
except TypeError as e:
print(e)
b = a + (4, 5)
print(b)
print(a * 2)
print(a)- 결과
- ‘tuple’ object does not support item assignment
(1, 2, 3, 4, 5)
(1, 2, 3, 1, 2, 3)
(1, 2, 3)
튜플의 요소에 값을 대입하면 TypeError가 발생합니다. 그러므로 목록의 append(), insert(), remove(), sort() 같은 함수도 존재하지 않습니다.
+와 *을 이용해 튜플을 연결하거나 반복할 수 있지만, 이는 기존 튜플을 바꾸는 것이 아니라 새로운 튜플을 생성하는 것입니다.
마지막 줄에서 a를 출력하면 +, * 연산을 한 뒤에도 (1, 2, 3) 그대로인 것을 확인할 수 있습니다.
a = (1, 2, 3)
b = a
a += (4,)
print(a, b, a is b)- 결과
- (1, 2, 3, 4) (1, 2, 3) False
튜플에도 +=를 사용할 수 있지만, 동작은 목록과 다릅니다.
목록의 +=는 기존 목록을 직접 늘렸지만, 튜플은 변경할 수 없으므로 a += (4,)는 a = a + (4,)와 똑같이 새 튜플을 만들어 a에 다시 연결합니다. 그래서 같은 튜플을 가리키던 b는 (1, 2, 3)으로 남고, a is b는 False가 됩니다.
a = (1, [2, 3])
a[1].append(4)
print(a)
print(hash((1, 2)) == hash((1, 2)))
try:
hash(a)
except TypeError as e:
print(e)- 결과
- (1, [2, 3, 4])
True
unhashable type: ‘list’
튜플 a의 1번 원소는 목록입니다. a[1] = [5]처럼 튜플의 칸을 다른 객체로 바꾸는 것은 불가능하지만, a[1].append(4)처럼 칸이 가리키는 목록의 내용을 바꾸는 것은 가능합니다.
- Tip : 튜플 안에 목록처럼 변경 가능한 객체가 들어있다면, 그 목록의 내용은 변경할 수 있습니다. 튜플이 보장하는 것은 어떤 객체를 담고 있는지가 바뀌지 않는다는 점입니다.
hash()는 객체의 해시값(Hash), 즉 내용으로부터 계산한 정수를 반환합니다. 사전과 집합은 이 해시값으로 원소의 저장 위치를 정하므로, 해시값을 계산할 수 있어야 사전의 key나 집합의 원소가 될 수 있습니다.
(1, 2)처럼 원소가 모두 변경 불가능한 튜플은 내용이 같으면 해시값도 항상 같습니다. 반면 목록을 담은 튜플은 내용이 바뀔 수 있으므로 해시값을 계산할 수 없어 unhashable type: 'list' 오류가 발생합니다.
패킹 & 언패킹
튜플이 가장 자주 쓰이는 곳은 사실 소괄호를 직접 쓰는 곳이 아니라, 여러 값을 한 번에 묶고 푸는 문법입니다. 이 문법은 목록, 문자열 등 다른 시퀀스에도 똑같이 적용됩니다.
point = 3, 4
x, y = point
print(point)
print(x, y)
first, *rest = (1, 2, 3, 4)
print(first, rest)
x, y = y, x
print(x, y)- 결과
- (3, 4)
3 4
1 [2, 3, 4]
4 3
여러 값을 쉼표로 나열해 하나의 튜플로 묶는 것을 패킹(Packing)이라 합니다.
반대로 x, y = 튜플처럼 튜플의 요소를 여러 변수에 나누어 할당하는 것을 언패킹(Unpacking)이라 합니다.
언패킹할 때는 변수의 개수와 요소의 개수가 같아야 하며, 다르면 ValueError가 발생합니다.
*변수를 사용하면 남는 요소들을 목록(List)으로 모아서 받을 수 있습니다.
x, y = y, x처럼 패킹과 언패킹을 함께 사용하면 임시 변수 없이 두 변수의 값을 교환할 수 있습니다.
값 교환이 가능한 이유는 Python이 오른쪽을 먼저 모두 계산하기 때문입니다. y, x가 먼저 튜플 (4, 3)으로 패킹된 뒤, 그 튜플이 x, y에 차례대로 언패킹됩니다.
try:
x, y = (1, 2, 3)
except ValueError as e:
print(e)
try:
x, y, z = (1, 2)
except ValueError as e:
print(e)
a, *b, c = 1, 2, 3, 4, 5
print(a, b, c)
*a, b = (1,)
print(a, b)- 결과
- too many values to unpack (expected 2, got 3)
not enough values to unpack (expected 3, got 2)
1 [2, 3, 4] 5
[] 1
변수가 2개인데 값이 3개면 too many values to unpack, 변수가 3개인데 값이 2개면 not enough values to unpack 오류가 발생합니다. 메시지 뒤의 (expected 2, got 3)은 “변수는 2개인데 값은 3개”라는 뜻입니다.
*변수는 맨 앞이나 맨 뒤뿐 아니라 가운데에도 둘 수 있습니다. a, *b, c는 첫 값과 마지막 값을 a, c에 주고, 나머지를 목록 b에 모읍니다.
남는 값이 없으면 *변수에는 빈 목록이 들어갑니다. 단, *변수는 한 번의 언패킹에서 하나만 사용할 수 있습니다.
def min_max(numbers):
return min(numbers), max(numbers)
result = min_max([3, 1, 4, 1, 5])
print(result)
low, high = min_max([3, 1, 4, 1, 5])
print(low, high)
points = [(0, 0), (3, 4), (1, 2)]
for x, y in points:
print(x + y)- 결과
- (1, 5)
1 5
0
7
3
함수에서 return 값1, 값2처럼 여러 값을 반환하면, 실제로는 두 값을 패킹한 튜플 하나가 반환됩니다. 그래서 result에는 (1, 5)가 저장되고, low, high = ...처럼 바로 언패킹하여 받을 수도 있습니다.
반복문에서도 언패킹을 사용할 수 있습니다. points의 각 원소는 (x, y) 튜플이므로, for x, y in points:는 반복할 때마다 튜플을 꺼내 x와 y로 나눕니다. 결과는 각 좌표의 합 0, 7, 3입니다.
- Tip : 함수와 반복문은 이후 강좌에서 자세히 다룹니다. 여기서는 “여러 값을 묶어 주고받을 때 튜플이 쓰인다”는 점만 기억합니다.
조사
튜플은 변경할 수 없으므로, 목록 메서드 중 원본을 바꾸는 메서드는 모두 없고 정보를 조회하는 메서드 index()와 count() 두 개만 가지고 있습니다. 내장 함수 len(), min(), max()와 in 연산자는 목록과 똑같이 사용할 수 있습니다.
a = (1, 2, 3, 4, 5)
print(len(a))
print(max(a))
print(min(a))
print(a.index(3))
print(a.count(1))
print(6 in a)- 결과
- 5
5
1
2
1
False
len()을 이용하여 튜플의 길이를 확인할 수 있습니다.
min()을 이용하여 튜플에서 최솟값을 가지는 원소의 값을 확인할 수 있습니다.
max()를 이용하여 튜플에서 최댓값을 가지는 원소의 값을 확인할 수 있습니다.
튜플.index()를 이용하여 해당 값이 가지는 위치를 확인 할 수 있습니다. (시작값=0)
튜플.count()를 이용하여 해당 값이 가지는 개수를 확인할 수 있습니다.
값 in 튜플을 이용하여 해당 값이 존재하는지 확인할 수 있습니다.
결과를 읽으면, 원소는 5개이고 최댓값은 5, 최솟값은 1이며, 값 3은 2번 위치에, 값 1은 1개 있고, 6은 없습니다.
튜플도 해시값으로 위치를 찾는 자료형이 아니므로, in, index(), count()는 목록처럼 처음부터 원소를 하나씩 비교합니다. 튜플 자체는 해시 가능하지만, 그것은 “튜플을 사전의 key로 쓸 수 있다”는 뜻이지 “튜플 안에서 빨리 찾을 수 있다”는 뜻이 아닙니다.
- Tip : 튜플의
index()도 값이 없으면ValueError가 발생하므로, 확실하지 않을 때는in으로 먼저 확인합니다.
print((1, 2, 3) < (1, 3))
print((1, 2) == (1, 2))
print((1, 2) == [1, 2])- 결과
- True
True
False
튜플끼리는 크기를 비교할 수 있으며, 목록처럼 앞쪽 원소부터 차례대로 비교합니다. (1, 2, 3)과 (1, 3)은 첫 원소가 같고 두 번째 원소 2 < 3에서 결정되므로 True입니다. 길이는 비교 기준이 아닙니다.
내용이 같아도 튜플과 목록은 자료형이 다르므로 (1, 2) == [1, 2]는 False입니다.
목록과 튜플
튜플은 원소를 바꿀 수 없지만, 필요하다면 목록으로 바꿔서 수정한 뒤 다시 튜플로 만들 수 있습니다. 또한 해시 가능하다는 장점을 살려 사전의 key로 사용할 수 있습니다.
a = (3, 1, 2)
b = list(a)
b.append(4)
b.sort()
a = tuple(b)
print(a)
print(sorted((3, 1, 2)))- 결과
- (1, 2, 3, 4)
[1, 2, 3]
list(튜플)로 변경 가능한 복사본을 만든 뒤 append(), sort()로 수정하고, tuple(목록)으로 다시 튜플을 만들었습니다. 원래 튜플이 바뀐 것이 아니라 새 튜플을 a에 다시 대입한 것입니다.
튜플에는 sort()가 없지만, 내장 함수 sorted()는 튜플도 정렬할 수 있습니다. 단, 결과는 튜플이 아니라 목록으로 반환됩니다.
location = {(37, 127): "서울", (35, 129): "부산"}
print(location[(37, 127)])
try:
{[37, 127]: "서울"}
except TypeError as e:
print(e)- 결과
- 서울
cannot use ‘list’ as a dict key (unhashable type: ‘list’)
(위도, 경도) 튜플은 해시 가능하므로 사전의 key가 될 수 있습니다. 같은 좌표를 목록 [37, 127]로 key에 넣으면, 목록은 해시 불가능하므로 TypeError가 발생합니다.
이처럼 여러 값을 순서대로 묶어 key로 써야 할 때는 튜플을 사용합니다. 사전은 제 12강에서 자세히 다룹니다.
import sys
print(sys.getsizeof([1, 2, 3]), sys.getsizeof((1, 2, 3)))- 결과
- 88 72
sys.getsizeof()는 객체 자체가 차지하는 메모리 크기를 바이트 단위로 반환합니다. 같은 원소를 담아도 튜플이 목록보다 작은 것을 볼 수 있습니다. 목록은 원소가 늘어날 것에 대비한 정보를 추가로 가지고 있지만, 튜플은 크기가 고정되어 있기 때문입니다.
- Tip : 출력되는 숫자는 64비트 Python 3.14.6 기준이며, Python 버전이나 운영체제에 따라 다를 수 있습니다.
목록과 튜플은 다음 기준으로 선택합니다.
| 기준 | 목록(List) | 튜플(Tuple) |
|---|---|---|
| 변경 | 가능 | 불가능 |
| 표기 | [1, 2] |
(1, 2) 또는 1, 2 |
| 메서드 | append(), sort() 등 다수 |
index(), count()만 |
| 해시 | 불가능 (사전 key·집합 원소 불가) | 원소가 모두 해시 가능하면 가능 |
| 주 용도 | 개수가 변하는 같은 종류의 데이터 모음 | 개수가 정해진 값의 묶음, 다중 반환, 사전 key |
정리
| 이름 | 설명 | 반환값/특징 |
|---|---|---|
(a, b), a, b |
튜플 생성 | 튜플을 만드는 것은 쉼표 |
(a,) |
원소가 하나인 튜플 | 쉼표 없으면 그냥 값 |
tuple(x) |
반복 가능한 객체를 튜플로 | 새 튜플 |
튜플[i], 튜플[start:end] |
인덱싱, 슬라이싱 | 원소, 새 튜플 |
튜플 + 튜플, 튜플 * n, += |
연결, 반복 | 항상 새 튜플 |
x, y = 튜플 |
언패킹 | 개수가 다르면 ValueError |
a, *b = 튜플 |
나머지 원소 모으기 | b는 목록 |
튜플.index(x) |
x의 첫 위치 |
정수, 없으면 ValueError |
튜플.count(x) |
x의 개수 |
정수 |
x in 튜플 |
존재 여부 | 논리값, 앞에서부터 차례로 비교 |
hash(튜플) |
해시값 | 원소가 모두 해시 가능할 때만 |
튜플은 변경할 수 없고, 순서가 있으며, 중복을 허용하는 자료형입니다. 바뀌지 않는다는 보장 덕분에 정해진 값의 묶음을 안전하게 전달할 수 있고, 원소가 모두 해시 가능하면 사전의 key나 집합의 원소로도 사용할 수 있습니다.
댓글 남기기