PUBLISHED

list와 dict

작성일: 2023.07.28

list와 dict

list

list는 자바스크립트의 array와 거의 비슷하지만, 파이썬답게 더 간결한 표기와 규칙을 제공한다. 음수 인덱싱, 슬라이싱, 그리고 가변 시퀀스라는 성질이 핵심이다. 특히 슬라이싱은 단순한 부분 추출을 넘어, 한 번에 여러 값을 치환하는 데도 쓰일 수 있어서 실제 코드에서는 “복사와 수정”을 동시에 처리하는 도구로 자주 등장한다. 다만 list는 가변 객체이기 때문에 참조(alias) 문제가 생기기 쉽고, 이 점을 이해하지 않으면 원치 않는 공유 변경이 발생한다.

리스트는 값의 순서가 중요하고 중복을 허용한다. 그래서 “순서가 있는 컬렉션”을 표현할 때 가장 직관적이다. 반면 검색이나 멤버십 테스트가 많아질수록 list는 느려지고, 그런 경우 set이나 dict가 더 적합해진다. 즉, list는 단순하고 범용적이지만, 용도를 분명히 해야 성능과 가독성을 동시에 챙길 수 있다.

untitled
CSS
newList = [1, 2, 3, 4]

newList[0] = 99         # 값 변경 (덮어쓰기)
newList[2]              # 값 반환
newList[-1]             # 마지막 값
newList[1:3]            # 1부터 2까지 슬라이스
newList[1:3] = [7, 8]   # 슬라이스 치환

자바스크립트와 달리 훨씬 더 직관적인 메서드 이름도 마음에 든다. 개인적으로 unshift가 0번 인덱스에 값을 넣는 행위라는 걸 기억하는 게 참 쉽지 않다. 파이썬은 append, insert처럼 “행동이 이름에 그대로 드러나는” 방식이라 코드의 의도가 잘 보인다. 이 작은 차이가 유지보수에서 큰 차이를 만든다.

untitled
CSS
newList.append(a)        # 마지막 인덱스로 a 값 추가
newList.extend([a, b])   # 여러 값을 한 번에 추가
newList.insert(i, v)     # i번 인덱스에 v 추가
newList.remove(a)        # 가장 먼저 a와 일치하는 item 삭제
newList.pop()            # 마지막 값 제거 후 반환
newList.clear()          # list 싹 비우기
del newList[i]           # i번 인덱스 삭제
del newList[a:b]         # a 인덱스부터 b 직전까지 삭제

다양한 함수와 키워드, 메서드를 사용하여 list에 대해 조사할 수도 있다. 이런 도구들은 리스트를 “데이터 구조”로서 다룰 때 굉장히 유용하다. 다만 list는 선형 구조이기 때문에, index나 in 연산은 길이에 비례해 느려진다. 빈번한 조회가 필요하다면 구조를 바꾸는 게 더 효과적이다.

untitled
PY
len(newList)         # list의 길이를 반환

2 in newList         # 존재 여부
2 not in newList     # 부존재 여부

newList.index(a)     # a 값을 가진 첫 인덱스 반환 (없으면 에러)
newList.count(a)     # list 내에 요소 a가 등장하는 횟수 반환

정렬은 list에서 자주 발생하는 작업이지만, 원본 변경 여부를 항상 의식해야 한다. sort는 제자리 정렬, sorted는 새 리스트 반환이라는 차이가 있다. 이 차이를 무시하면 의도치 않게 원본 데이터가 바뀌는 문제가 생긴다. 특히 여러 곳에서 같은 리스트를 참조하는 상황에서는 더 위험하다.

untitled
PY
# 기존 list가 변경됨
newList.sort()
newList.reverse()

# 새로운 list를 반환함
sorted(newList, reverse=True)  # reverse=True는 역순이 아니라 '내림차순'
list(reversed(newList))

# 동일한 list를 반환하되, 참조형 aliasing 문제 해결
list(newList)
newList[:]   # 얕은 복사

리스트를 복사할 때는 얕은 복사와 깊은 복사의 차이를 구분해야 한다. 리스트 안에 또 리스트나 dict가 들어있는 구조라면 얕은 복사는 내부 객체를 공유한다. 이때는 copy 모듈을 사용하거나, 구조 자체를 다시 만드는 게 안전하다. 결국 list는 단순해 보이지만, “참조를 공유한다”는 사실을 항상 염두에 둬야 한다.

 

dict

dict는 자바스크립트의 object와 비슷하게 key: value 쌍으로 구성된다. 다만 파이썬의 dict는 “해시 테이블” 기반이라는 점을 명확히 인지해야 한다. 키는 해시 가능해야 하고, 값은 무엇이든 될 수 있다. 그리고 파이썬 3.7 이후 dict는 삽입 순서를 보장한다. 이 사실 때문에 dict는 단순한 매핑을 넘어 “순서가 있는 매핑”으로도 쓰인다.

dict의 핵심은 안전한 접근이다. 키가 없을 때의 처리를 어떻게 하느냐가 설계를 바꾼다. 직접 접근은 빠르고 명확하지만 예외가 발생하고, get은 안정적이지만 의도를 흐릴 수 있다. 그래서 어떤 경우에는 “없으면 에러”가 더 정확한 설계이기도 하다. 결국 dict는 데이터 구조이면서도 정책을 담는 도구다.

untitled
PY
newDict = {
    "name": "Ayden",
    "age": 28,
    "bornYear": 1994,
    "most_famous_work": {
        "title": "사서 에밀리 힐덴베르크의 우울",
        "language": "korean"
    }
}

newDict["name"]          # 직접 접근
newDict.get("age")       # 안전한 접근
newDict.get("none", 0)   # 기본값

newDict.items()          # key/value 쌍
newDict.keys()           # key 목록
newDict.values()         # value 목록

기본적인 접근법은 list와 비슷하지만, dict는 안전한 사용을 위한 메서드가 훨씬 많다. update는 병합, setdefault는 “없으면 생성” 정책을 간단히 구현한다. 이런 메서드를 잘 쓰면 if 문이 줄어들고, 데이터 병합이 명확해진다.

untitled
PY
# 병합/업데이트
my_dict = {'a': 1, 'b': 2}
my_dict.update({'b': 3, 'c': 4})

# 안전한 조회
print(my_dict.get('a'))          # 1
print(my_dict.get('z', '없음'))  # '없음'

# 없으면 생성
my_dict.setdefault('d', 0)

# 파이썬 3.9+ 병합 연산자
merged = my_dict | {'e': 5}

dict도 복사 문제에서 자유롭지 않다. 얕은 복사는 내부 중첩 객체를 공유한다. 그래서 중첩 구조를 안전하게 복사하려면 deepcopy가 필요하다. 다만 deepcopy는 비용이 크므로, 정말 필요한 순간에만 쓰는 게 좋다. 결국 “값을 복제할 것인가, 참조를 공유할 것인가”는 dict 설계의 가장 중요한 선택이다.

untitled
CSS
import copy

original = {'a': 1, 'b': {'c': 2}}
deep_copy = copy.deepcopy(original)

deep_copy['b']['c'] = 99
print(original)  # {'a': 1, 'b': {'c': 2}} (원본에 영향 없음)

dict는 단순히 값을 저장하는 곳이 아니라, “키 설계”가 핵심이다. 키가 무엇을 의미하는지, 키가 없다면 어떤 정책을 취할지, 순서를 신뢰할지. 이런 선택이 결국 데이터 모델을 정의한다.

 

collections (컬렉션)

파이썬 표준 라이브러리의 collections 모듈은 “기본 자료구조를 더 강하게 만든 버전”이라고 이해하면 쉽다. list와 dict만으로도 대부분의 문제를 해결할 수 있지만, 실제 프로젝트에서는 더 명확한 의도를 담는 구조가 필요하다. collections는 그런 순간을 위해 존재한다. 즉, 같은 데이터라도 어떤 규약을 부여할지에 따라 코드의 읽기와 성능이 달라진다.

여기서 중요한 건 “새로운 자료구조”가 아니라 “기존 구조를 특정 목적에 맞게 제한하거나 확장한 구조”라는 점이다. 그래서 collections를 쓰는 순간 의도가 명확해지고, 유지보수 시 실수가 줄어든다. 다만 아무 때나 쓰면 복잡도만 늘어날 수 있으니, 명확한 사용 이유가 있어야 한다.

untitled
CSS
from collections import Counter

data = ["a", "b", "a", "c", "b", "a"]
count = Counter(data)
print(count)            # Counter({'a': 3, 'b': 2, 'c': 1})
print(count.most_common(1))

Counter는 등장 횟수를 세는 데 최적화된 dict다. 직접 dict를 업데이트하는 방식보다 의도가 명확하고, most_common 같은 메서드가 기본 제공된다. 다만 Counter도 결국 dict이기 때문에, 음수 카운트가 허용된다는 점을 알고 써야 한다.

untitled
SH
from collections import defaultdict

groups = defaultdict(list)
groups["fruit"].append("apple")
groups["fruit"].append("banana")
print(groups["fruit"])  # ['apple', 'banana']

defaultdict는 “키가 없으면 기본값을 만든다”는 정책을 자료구조에 박아 넣는다. if 키 검사 코드가 줄어들고, 데이터 흐름이 깔끔해진다. 반면 의도치 않게 키가 생성될 수 있기 때문에, 읽기보다 쓰기가 많은 코드에서 더 적합하다.

untitled
CSS
from collections import deque

q = deque([1, 2, 3])
q.append(4)       # 오른쪽 추가
q.appendleft(0)   # 왼쪽 추가
q.pop()           # 오른쪽 제거
q.popleft()       # 왼쪽 제거

deque는 양쪽에서 빠른 추가/삭제를 제공하는 큐다. list의 pop(0)은 느리지만 deque는 O(1)에 가깝게 동작한다. 따라서 큐나 슬라이딩 윈도우 같은 작업에서는 deque가 훨씬 적합하다. 결국 collections는 성능과 의도를 동시에 잡아주는 도구 모음이고, 필요할 때 꺼내 쓰는 것이 가장 현명하다.