Python 시퀀스·세트·딕셔너리·반복
Python의 리스트·튜플·range·세트·딕셔너리를 순서·중복·변경 가능성으로 구분한다. 인덱싱과 슬라이싱, 별칭과 복사, 반복문과 컴프리헨션에서 원소와 상태가 바뀌는 과정을 추적한다. 수치 합산과 원본 객체의 상태 변화도 구분한다.
Python의 리스트·튜플·range·세트·딕셔너리를 순서·중복·변경 가능성으로 구분한다. 인덱싱과 슬라이싱, 별칭과 복사, 반복문과 컴프리헨션에서 원소와 상태가 바뀌는 과정을 추적한다.
그림으로 확인하기

컨테이너를 고르는 네 가지 기준
자료구조 이름을 먼저 외우기보다 다음 네 질문을 순서대로 적용한다.
- 순서와 위치가 필요한가? 정수 위치로 원소를 찾고 슬라이스해야 하면 시퀀스를 검토한다.
- 중복을 허용하는가? 같은 원소를 한 번만 보관하려면 세트를 검토한다.
- 키로 값을 찾아야 하는가? 사번·상품 코드·단어처럼 식별자를 통해 값을 찾으려면 딕셔너리를 검토한다.
- 컨테이너 자체를 바꿔야 하는가? 원소 추가·삭제·교체가 필요하면 가변 컨테이너를 선택한다.
| 형 | 순서·위치 | 중복 | 변경 가능성 | 주된 조회 기준 |
|---|---|---|---|---|
list | 있음 | 허용 | 가변 | 정수 인덱스·순회 |
tuple | 있음 | 허용 | 불변 | 정수 인덱스·순회 |
range | 규칙에 따른 순서 | 값이 겹치지 않는 산술 수열 | 불변 | 정수 인덱스·순회 |
set | 위치 계약 없음 | 허용하지 않음 | 가변 | 원소 포함 여부 |
frozenset | 위치 계약 없음 | 허용하지 않음 | 불변 | 원소 포함 여부 |
dict | 삽입 순서 보존 | 키는 고유, 값은 중복 가능 | 가변 | 해시 가능한 키 |
dict와 set도 반복할 수 있으므로 이터러블이지만 시퀀스는 아니다. 반복 가능하다는 사실과 정수 인덱스로 위치 접근이 가능하다는 사실을 같은 뜻으로 보면 안 된다.
시퀀스의 공통 연산
Python의 기본 시퀀스에는 리스트, 튜플, range가 있다. 문자열도 시퀀스이지만 이 이론에서는 컨테이너 선택과 상태 추적에 필요한 규칙만 다룬다.
| 연산 | 의미 | 주의점 |
|---|---|---|
x in s | x와 같은 원소가 있는지 검사 | 위치를 반환하지 않고 True 또는 False를 반환한다. |
s + t | 같은 계열 시퀀스를 연결 | 새 시퀀스를 만들며 range는 연결을 지원하지 않는다. |
s * n | 원소 참조를 n회 반복한 새 시퀀스 생성 | 중첩 가변 객체를 실제로 n개 복제하지 않는다. |
s[i] | 0부터 시작하는 위치 i의 원소 | 범위를 벗어나면 IndexError가 발생한다. |
s[i:j:k] | 시작·끝·간격으로 부분 시퀀스 선택 | j는 포함하지 않으며 k는 0일 수 없다. |
len(s) | 원소 수 | range도 길이를 계산할 수 있다. |
s.count(x) | x와 같은 원소의 개수 | 대부분의 기본 시퀀스가 제공한다. |
s.index(x) | 첫 번째 x의 위치 | 찾지 못하면 ValueError가 발생한다. |
다음 코드는 같은 값을 찾는 연산과 위치를 찾는 연산을 구분한다.
values = [10, 20, 10, 30]
print(10 in values) # True
print(values.count(10)) # 2
print(values.index(10)) # 0
index()는 모든 위치를 반환하지 않고 첫 번째 위치만 반환한다. values.index(99)처럼 없는 값을 찾으면 -1이 아니라 ValueError가 발생한다.
리스트: 가변 시퀀스의 상태 변화
원소 추가·삭제·교체
리스트는 원소를 직접 바꾸는 가변 시퀀스다.
| 연산·메서드 | 상태 변화 | 반환값·예외 |
|---|---|---|
a.append(x) | 끝에 x 하나를 원소로 추가 | None |
a.extend(iterable) | 이터러블의 각 원소를 끝에 이어 붙임 | None |
a.insert(i, x) | 위치 i 앞에 x 삽입 | None |
a.remove(x) | 값이 같은 첫 원소 삭제 | 반환값 None, 없으면 ValueError |
a.pop() | 마지막 원소 삭제 | 삭제한 원소 반환, 비어 있으면 IndexError |
a.pop(i) | 위치 i의 원소 삭제 | 삭제한 원소 반환 |
a.clear() | 모든 원소 삭제 | None |
del a[i] | 위치 i의 원소 삭제 | 값을 반환하지 않는 문장 |
a[i] = x | 위치 i의 원소 교체 | 대입문 자체의 반환값 없음 |
append와 extend는 결과 모양이 다르다.
first = [1, 2]
first.append([3, 4])
print(first) # [1, 2, [3, 4]]
second = [1, 2]
second.extend([3, 4])
print(second) # [1, 2, 3, 4]
append([3, 4])는 리스트 한 개를 원소로 추가한다. extend([3, 4])는 전달된 이터러블을 순회해 3과 4를 각각 추가한다. 문자열을 extend하면 문자 단위로 추가된다는 점도 같은 원리다.
letters = ["A"]
letters.extend("BC")
print(letters) # ['A', 'B', 'C']
값을 삭제하는 remove와 위치를 삭제하는 pop
values = [10, 20, 10, 30]
values.remove(10)
removed = values.pop(1)
print(values) # [20, 30]
print(removed) # 10
remove(10)은 첫 번째10만 지워[20, 10, 30]이 된다.- 이어서
pop(1)은 현재 위치 1의10을 지우고 그 값을 반환한다. - 최종 리스트는
[20, 30]이다.
remove의 인자는 값이고 pop의 인자는 위치다. 이 차이를 거꾸로 쓰는 선지가 자주 나온다.
제자리 변경 메서드와 새 결과를 만드는 함수
리스트의 sort()와 reverse()는 원본을 제자리에서 바꾸고 None을 반환한다. 반면 sorted(iterable)은 원본을 그대로 두고 새 리스트를 반환한다.
values = [3, 1, 2]
result = values.sort()
print(values) # [1, 2, 3]
print(result) # None
따라서 다음 코드는 정렬된 리스트를 result에 저장하지 못한다.
values = [3, 1, 2]
result = values.sort() # result는 None
새 결과가 필요하면 다음처럼 쓴다.
values = [3, 1, 2]
result = sorted(values)
print(values) # [3, 1, 2]
print(result) # [1, 2, 3]
sort()와 sorted()는 모두 같은 정렬 키를 가진 원소의 상대적 순서를 유지하는 안정 정렬을 제공한다. 그러나 반환값과 원본 변경 여부는 다르다.
+와 +=의 객체 동일성
리스트에서 +는 새 리스트를 만들고, +=는 일반적으로 기존 리스트를 제자리에서 확장한다.
left = [1, 2]
alias = left
left += [3]
print(left) # [1, 2, 3]
print(alias) # [1, 2, 3]
print(left is alias) # True
alias도 같은 리스트를 가리키므로 변경을 함께 본다. 반면 튜플은 불변이라 +=가 새 튜플을 만들어 이름을 다시 연결한다.
left = (1, 2)
alias = left
left += (3,)
print(left) # (1, 2, 3)
print(alias) # (1, 2)
print(left is alias) # False
튜플과 range
튜플은 괄호보다 쉼표가 핵심이다
튜플은 불변 시퀀스다. 한 원소 튜플은 괄호만으로 만들 수 없고 쉼표가 필요하다.
print(type((10))) # <class 'int'>
print(type((10,))) # <class 'tuple'>
single = 10,
print(type(single)) # <class 'tuple'>
(10)은 우선순위를 위한 괄호일 뿐이다. (10,) 또는 10,처럼 쉼표가 있어야 한 원소 튜플이 된다.
튜플의 불변성은 튜플이 각 위치에서 어떤 객체를 가리키는지 바꿀 수 없다는 뜻이다. 원소가 리스트 같은 가변 객체이면 그 객체 내부는 바뀔 수 있다.
record = ("A", [1, 2])
record[1].append(3)
print(record) # ('A', [1, 2, 3])
# record[0] = "B" # TypeError: 튜플 위치 교체 불가
이 튜플은 내부에 해시 불가능한 리스트를 포함하므로 딕셔너리 키나 세트 원소로 사용할 수 없다. 불변 컨테이너이면 언제나 해시 가능하다는 설명은 틀리다.
패킹과 언패킹
쉼표로 여러 값을 묶으면 튜플 패킹이 일어나고, 여러 대상에 나누어 대입하면 언패킹이 일어난다.
point = 3, 4
x, y = point
print(point) # (3, 4)
print(x, y) # 3 4
별표 대상은 남은 원소를 리스트로 받는다.
first, *middle, last = [10, 20, 30, 40]
print(first) # 10
print(middle) # [20, 30]
print(last) # 40
대상 수가 맞지 않고 별표 대상도 없으면 ValueError가 발생한다.
range는 숫자 목록이 아니라 불변 수열 표현이다
range(stop) 또는 range(start, stop, step)은 산술 규칙을 표현하는 불변 시퀀스다. 기본값은 start=0, step=1이고 stop은 포함하지 않는다. step=0이면 ValueError가 발생한다.
print(list(range(5))) # [0, 1, 2, 3, 4]
print(list(range(2, 8, 2))) # [2, 4, 6]
print(list(range(5, 0, -2))) # [5, 3, 1]
print(list(range(2, 8, -1))) # []
양의 간격인데 시작값이 끝값 이상이거나, 음의 간격인데 시작값이 끝값 이하이면 빈 수열이 된다. range는 모든 값을 리스트에 미리 저장하는 방식이 아니라 시작·끝·간격 규칙으로 값을 계산하므로 큰 범위를 표현할 때 전체 목록을 만들 필요가 없다.
range도 시퀀스이므로 인덱싱, 음수 인덱싱, 슬라이싱, 포함 검사를 지원한다.
numbers = range(10, 20, 2)
print(numbers[0]) # 10
print(numbers[-1]) # 18
print(list(numbers[1:4])) # [12, 14, 16]
print(16 in numbers) # True
두 range 객체는 생성 인자가 달라도 실제로 표현하는 수열이 같으면 동등할 수 있다.
print(range(0, 3, 2) == range(0, 4, 2)) # True
두 객체 모두 [0, 2]를 표현하기 때문이다.
인덱싱과 슬라이싱
단일 인덱스
인덱스는 0부터 시작한다. 음수 인덱스는 끝에서부터 센다.
values = ["A", "B", "C", "D", "E", "F"]
print(values[0]) # A
print(values[-1]) # F
print(values[-3]) # D
길이가 6인 시퀀스에서 -1은 위치 5, -2는 위치 4로 해석한다. 단일 인덱스가 범위를 벗어나면 IndexError가 발생한다.
values = [10, 20, 30]
# print(values[3]) # IndexError
# print(values[-4]) # IndexError
슬라이스의 세 요소
슬라이스는 s[start:stop:step] 형태다.
start위치는 포함한다.stop위치는 포함하지 않는다.step은 이동 간격이며 0일 수 없다.step이 양수이면 왼쪽에서 오른쪽으로 진행한다.step이 음수이면 오른쪽에서 왼쪽으로 진행한다.
values = ["A", "B", "C", "D", "E", "F"]
print(values[1:5]) # ['B', 'C', 'D', 'E']
print(values[1:5:2]) # ['B', 'D']
print(values[-4:-1]) # ['C', 'D', 'E']
print(values[4:1:-1]) # ['E', 'D', 'C']
print(values[::-2]) # ['F', 'D', 'B']
values[4:1:-1]은 위치 4에서 시작해 1보다 큰 위치까지만 이동하므로 위치 4, 3, 2를 선택한다. 음의 간격에서도 stop은 포함하지 않는다.
start와 stop을 생략했을 때의 기본 방향은 step의 부호에 따라 달라진다.
values = [0, 1, 2, 3, 4]
print(values[:3]) # [0, 1, 2]
print(values[2:]) # [2, 3, 4]
print(values[::-1]) # [4, 3, 2, 1, 0]
values[::-1]은 음의 간격으로 전체를 순회해 역순의 새 시퀀스를 만든다. 이를 values[0:len(values):-1]과 같은 뜻으로 보면 안 된다.
슬라이스 경계는 조정되지만 간격 0은 오류다
단일 인덱스와 달리 슬라이스의 경계는 실제 길이를 벗어나도 가능한 범위로 조정된다.
values = [10, 20, 30]
print(values[100:200]) # []
print(values[-100:100]) # [10, 20, 30]
그러나 간격 0은 진행 방향을 정할 수 없으므로 오류다.
values = [10, 20, 30]
# values[::0] # ValueError
리스트 슬라이스 대입
리스트는 슬라이스 범위를 다른 이터러블의 원소로 교체할 수 있다.
values = [0, 1, 2, 3, 4]
values[1:4] = [10, 20]
print(values) # [0, 10, 20, 4]
간격이 1인 슬라이스 대입은 교체하는 원소 수가 달라도 리스트 길이가 조정된다. 그러나 간격이 1이 아닌 확장 슬라이스 대입은 선택된 위치 수와 새 원소 수가 같아야 한다.
values = [0, 1, 2, 3, 4, 5]
values[::2] = [10, 20, 30]
print(values) # [10, 1, 20, 3, 30, 5]
# values[::2] = [7, 8] # ValueError: 선택 위치는 3개인데 값은 2개
이름 연결, 객체 동일성, 복사
대입은 복사가 아니다
Python에서 이름은 객체에 연결된다. 다음 두 이름은 같은 리스트를 가리킨다.
original = [1, 2]
alias = original
alias.append(3)
print(original) # [1, 2, 3]
print(alias is original) # True
==는 일반적으로 값의 동등성을 검사하고, is는 두 이름이 같은 객체를 가리키는지 검사한다.
left = [1, 2]
right = [1, 2]
print(left == right) # True
print(left is right) # False
숫자나 문자열의 값 비교에 is를 쓰면 구현의 객체 재사용에 의존할 수 있다. 값 비교에는 ==, None 같은 단일 객체 확인에는 is를 사용한다.
얕은 복사
리스트 전체 슬라이스, list.copy(), list(existing_list)는 바깥 리스트를 새로 만들지만 원소 객체까지 복제하지 않는다.
original = [[1], [2]]
shallow = original.copy()
print(shallow is original) # False
print(shallow[0] is original[0]) # True
shallow[0].append(9)
print(original) # [[1, 9], [2]]
print(shallow) # [[1, 9], [2]]
바깥 리스트는 서로 다르지만 첫 번째 내부 리스트는 공유한다. 따라서 내부 리스트의 변경이 양쪽에서 보인다.
깊은 복사
중첩 가변 객체까지 독립적으로 복사해야 할 때 copy.deepcopy()를 사용할 수 있다.
import copy
original = [[1], [2]]
deep = copy.deepcopy(original)
deep[0].append(9)
print(original) # [[1], [2]]
print(deep) # [[1, 9], [2]]
깊은 복사가 항상 정답인 것은 아니다. 의도적으로 공유해야 하는 객체까지 복제할 수 있으므로 어떤 수준을 독립시킬지 먼저 정해야 한다.
시퀀스 반복의 공유 함정
* 연산은 원소 객체를 깊게 복제하지 않고 같은 참조를 여러 위치에 반복한다.
rows = [[0, 0]] * 3
rows[0][0] = 9
print(rows) # [[9, 0], [9, 0], [9, 0]]
세 행이 서로 다른 내부 리스트가 아니라 같은 내부 리스트를 세 번 가리키기 때문이다. 독립된 행이 필요하면 반복할 때마다 새 리스트를 만든다.
rows = [[0, 0] for _ in range(3)]
rows[0][0] = 9
print(rows) # [[9, 0], [0, 0], [0, 0]]
세트의 기본 연산
생성과 기본 성질
세트는 해시 가능한 원소의 중복 없는 집합이다.
values = {10, 20, 10, 30}
print(len(values)) # 3
print(20 in values) # True
빈 중괄호 {}는 빈 세트가 아니라 빈 딕셔너리다. 빈 세트는 set()으로 만든다.
print(type({})) # <class 'dict'>
print(type(set())) # <class 'set'>
세트에는 위치가 없으므로 다음 연산을 지원하지 않는다.
values = {10, 20, 30}
# values[0] # TypeError
# values[0:2] # TypeError
반복은 가능하지만 출력·반복 순서를 정답 계산에 사용하면 안 된다. 결정적인 출력이 필요하면 비교 가능한 원소에 한해 sorted(values)처럼 명시적으로 정렬한다.
집합 연산
| 연산 | 기호 | 결과 |
|---|---|---|
| 합집합 | a | b | 어느 한쪽에라도 있는 원소 |
| 교집합 | a & b | 양쪽 모두에 있는 원소 |
| 차집합 | a - b | a에는 있고 b에는 없는 원소 |
| 대칭 차집합 | a ^ b | 한쪽에만 있는 원소 |
| 부분 집합 | a <= b | a의 모든 원소가 b에 있는지 검사 |
| 진부분 집합 | a < b | 부분 집합이면서 서로 다른지 검사 |
| 서로소 | a.isdisjoint(b) | 공통 원소가 없는지 검사 |
left = {1, 2, 3}
right = {3, 4}
print(sorted(left | right)) # [1, 2, 3, 4]
print(sorted(left & right)) # [3]
print(sorted(left - right)) # [1, 2]
print(sorted(left ^ right)) # [1, 2, 4]
연산자형 &, |, -, ^는 다른 피연산자도 세트여야 한다. 메서드형 intersection, union, difference 등은 일반 이터러블도 인자로 받을 수 있다.
letters = set("abc")
print(letters.intersection("cde")) # {'c'}와 동등한 세트
# letters & "cde" # TypeError
세트 자체의 출력 순서는 고정하지 않으므로 위 결과를 문자열 모양 그대로 암기하지 않는다.
원소 추가·삭제
| 메서드 | 동작 | 원소가 없을 때 |
|---|---|---|
s.add(x) | 원소 x 추가 | 이미 있으면 변화 없음 |
s.update(iterable) | 여러 원소 추가 | 각 원소를 순회해 추가 |
s.remove(x) | 원소 x 삭제 | KeyError |
s.discard(x) | 원소 x 삭제 | 아무 일도 하지 않음 |
s.pop() | 임의의 원소 하나 삭제 후 반환 | 빈 세트이면 KeyError |
s.clear() | 모든 원소 삭제 | 빈 세트가 됨 |
set.pop()을 리스트의 pop()처럼 마지막 원소 삭제로 해석하면 안 된다. 세트에는 마지막 위치라는 계약이 없다.
딕셔너리: 고유 키와 값의 매핑
키와 값
딕셔너리는 해시 가능한 키를 임의의 값에 연결한다.
student = {"id": 17, "name": "Kim", "scores": [80, 90]}
- 키는 고유해야 한다.
- 키는 해시 가능해야 한다.
- 값은 해시 가능할 필요가 없으며 중복되어도 된다.
- 같은 키에 다시 대입하면 새 항목을 추가하지 않고 기존 값을 갱신한다.
scores = {"A": 10, "B": 20}
scores["A"] = 99
print(scores) # {'A': 99, 'B': 20}
print(len(scores)) # 2
키 조회와 기본값
| 표현 | 키가 있을 때 | 키가 없을 때 | 딕셔너리 변경 |
|---|---|---|---|
d[key] | 값 반환 | KeyError | 없음 |
d.get(key) | 값 반환 | None | 없음 |
d.get(key, default) | 값 반환 | default | 없음 |
d.setdefault(key, default) | 기존 값 반환 | default를 삽입하고 반환 | 없을 때 삽입 |
key in d | 키 포함 여부 반환 | False | 없음 |
get()은 기본값을 반환해도 키를 삽입하지 않는다.
counts = {}
print(counts.get("red", 0)) # 0
print(counts) # {}
반면 setdefault()는 키가 없으면 실제 항목을 만든다.
groups = {}
items = groups.setdefault("A", [])
items.append(10)
print(groups) # {'A': [10]}
in은 키를 검사한다
scores = {"Kim": 90, "Lee": 80}
print("Kim" in scores) # True
print(90 in scores) # False
print(90 in scores.values()) # True
print(("Kim", 90) in scores.items()) # True
딕셔너리를 직접 반복해도 기본적으로 키가 나온다.
scores = {"Kim": 90, "Lee": 80}
for key in scores:
print(key)
키와 값을 함께 사용하려면 items()를 사용한다.
scores = {"Kim": 90, "Lee": 80}
for name, score in scores.items():
print(name, score)
삽입 순서는 보존하지만 위치 인덱스는 아니다
딕셔너리는 키가 처음 삽입된 순서를 보존한다. 기존 키의 값을 갱신해도 그 키의 위치는 바뀌지 않는다. 키를 삭제한 뒤 다시 삽입하면 새 항목으로서 끝에 놓인다.
values = {"a": 1, "b": 2, "c": 3}
values["a"] = 10
print(list(values)) # ['a', 'b', 'c']
del values["b"]
values["b"] = 20
print(list(values)) # ['a', 'c', 'b']
그러나 삽입 순서 보존은 시퀀스 인덱싱을 의미하지 않는다.
values = {"a": 1, "b": 2}
# values[0] # 키 0을 찾으므로 KeyError
딕셔너리의 동등성은 삽입 순서가 아니라 같은 키가 같은 값에 대응하는지로 판단한다.
left = {"a": 1, "b": 2}
right = {"b": 2, "a": 1}
print(left == right) # True
딕셔너리 키와 세트 원소
딕셔너리의 키와 세트 원소에는 해시 가능한 객체를 사용한다. 정수·문자열은 대표적으로 가능하지만, 변경 가능한 리스트나 딕셔너리는 그대로 키나 세트 원소로 사용할 수 없다. 튜플은 내부 원소도 해시 가능할 때 사용할 수 있다.
반복과 이터레이터의 기본
리스트·튜플·문자열·range·딕셔너리 등은 for 문으로 원소를 순회할 수 있다. 이터레이터는 순회 위치를 유지하며 next()로 다음 값을 얻는 객체이다. 더 이상 값이 없으면 StopIteration으로 종료를 알리고, for 문은 이를 반복 종료로 처리한다. 한 번 소진된 같은 이터레이터는 자동으로 처음으로 돌아가지 않는다.
enumerate(values)는 원소의 순번과 값을 함께 제공한다. zip(a, b)는 같은 차례의 원소를 묶고 기본적으로 더 짧은 입력이 끝나면 멈춘다.
names = ["Kim", "Lee", "Park"]
scores = [90, 80]
print(list(enumerate(names, start=1)))
print(list(zip(names, scores)))
출력:
[(1, 'Kim'), (2, 'Lee'), (3, 'Park')]
[('Kim', 90), ('Lee', 80)]
딕셔너리를 직접 반복하면 키를 얻는다. 키와 값을 함께 사용하려면 items()를 사용한다. 반복 중 딕셔너리·세트의 크기를 바꾸거나 리스트에서 원소를 지우면 오류나 예상하지 못한 원소 누락이 생길 수 있으므로 새 결과를 구성하는 편이 명확하다.
컴프리헨션
리스트·세트·딕셔너리 컴프리헨션
컴프리헨션은 이터러블을 순회하면서 원소를 변환하거나 조건으로 거른 결과를 새 컨테이너에 담는다.
numbers = [1, 2, 3, 4]
squares = [x * x for x in numbers]
even_squares = [x * x for x in numbers if x % 2 == 0]
unique_remainders = {x % 3 for x in numbers}
square_map = {x: x * x for x in numbers}
print(squares) # [1, 4, 9, 16]
print(even_squares) # [4, 16]
print(sorted(unique_remainders)) # [0, 1, 2]
print(square_map) # {1: 1, 2: 4, 3: 9, 4: 16}
[...]컴프리헨션은 리스트를 만든다.{expression for ...}는 세트를 만든다.{key: value for ...}는 딕셔너리를 만든다.- 빈 중괄호
{}는 컴프리헨션이 아니라 빈 딕셔너리다.
여러 for와 if의 순서
다음 컴프리헨션은 왼쪽부터 중첩 반복문으로 읽는다.
pairs = [
(x, y)
for x in [1, 2]
for y in [10, 20]
if x + y >= 12
]
print(pairs) # [(1, 20), (2, 10), (2, 20)]
동등한 일반 반복문은 다음과 같다.
pairs = []
for x in [1, 2]:
for y in [10, 20]:
if x + y >= 12:
pairs.append((x, y))
for와 if의 순서를 임의로 바꾸면 결과가 달라질 수 있다. 출력 문제에서는 컴프리헨션을 일반 반복문 형태로 풀어 쓰면 안전하다.
컴프리헨션 변수의 범위
Python 3에서 컴프리헨션의 반복 변수는 컴프리헨션 내부 범위에 속하므로 바깥의 같은 이름을 덮어쓰지 않는다.
x = 100
values = [x * 2 for x in [1, 2, 3]]
print(x) # 100
print(values) # [2, 4, 6]
일반 for 문의 반복 변수가 반복 뒤에도 남는 것과 구분한다.
예제 A — 시퀀스·슬라이스·복사
import copy
values = [10, 20, 30, 40, 50]
alias = values
shallow = values[:]
alias[0] = 99
shallow.append(60)
print(values)
print(shallow)
print(values is alias, values is shallow)
nested = [[1], [2]]
nested_copy = nested.copy()
nested_copy[0].append(9)
print(nested)
deep_copy = copy.deepcopy(nested)
deep_copy[0].append(8)
print(nested)
print(deep_copy)
print(values[1:5:2])
print(values[::-1])
예상 출력:
[99, 20, 30, 40, 50]
[10, 20, 30, 40, 50, 60]
True False
[[1, 9], [2]]
[[1, 9], [2]]
[[1, 9, 8], [2]]
[20, 40]
[50, 40, 30, 20, 99]
핵심 추적:
alias는values와 같은 리스트를 가리킨다.shallow는 새 바깥 리스트이므로alias[0] = 99의 영향을 받지 않는다.nested_copy는 얕은 복사이므로 내부 리스트를 공유한다.deep_copy는 내부 리스트까지 복사하므로8추가가 원본에 반영되지 않는다.
수치 원소를 합하는 sum
sum(iterable)은 수치 원소들을 더한 값을 반환한다. 시작값을 생략하면 0이므로 빈 입력의 합은 0이다. sum(iterable, start)는 start에 각 원소를 더한다. 예를 들어 sum([2, 4], 10)은 16이다. 여기서는 정수 원소의 합을 기준으로 계산하며 문자열 연결에 sum을 사용하지 않는다.
리스트에 중복된 값이 있으면 각각 더한다. 먼저 set으로 바꾸면 중복 제거 후의 원소들을 더하므로 합이 달라질 수 있다. 딕셔너리에 sum(d)를 적용하면 키를 순회하며, 값의 합에는 sum(d.values())를 사용한다. 여러 키가 같은 값을 가지면 values에는 그 값이 각 항목에 대해 나타난다.
sum은 리스트 원소를 수정하지 않는다. 이미 변수에 저장한 정수 합계가 나중의 리스트 변경에 따라 자동 재계산되는 것도 아니다. 컴프리헨션으로 만든 리스트를 합할 때는 반복 범위, 필터 조건, 원소 계산 순서로 실제 결과 원소를 먼저 구한다.