2장 리스트와 딕셔너리
- 딕셔너리(Dictionary)는 검색에 사용할 키(key)와 키에 연관된 값(value)을 저장하는 구조이다.
- 내부적으로 해시 테이블(Hash Table)을 사용하며, 평균적으로 O(1)의 시간 복잡도로 원소를 삽입 및 검색할 수 있다.
- 동적인 데이터를 관리할 때 가장 이상적인 자료구조이다.
2-11 시퀀스를 슬라이싱하는 방법을 익혀라
- 슬라이싱(Slicing)을 사용하면 리스트 등의 시퀀스를 특정 부분만 잘라서 쉽게 접근할 수 있다.
- 슬라이싱을 할 때 가독성을 높이는 규칙:
- 리스트의 맨 앞부터 슬라이싱할 때
0
을 생략해야 한다. (예:a[:5]
) - 리스트의 끝까지 슬라이싱할 때 끝 인덱스를 생략해야 한다. (예:
a[5:]
)
- 리스트의 맨 앞부터 슬라이싱할 때
- 슬라이싱 결과는 완전히 새로운 리스트이며, 원래 리스트에 영향을 주지 않는다.
기억해야 할 내용:
- 슬라이싱은 간결하게 작성하라. 시작 인덱스에
0
을 넣거나 끝 인덱스에 리스트 길이를 넣지 않는다. - 범위를 초과하는 인덱스를 허용한다. 예를 들어
a[:20]
이나a[-20:]
과 같이 범위를 벗어난 값도 사용할 수 있다. - 리스트 슬라이스에 대입하면 원래 시퀀스의 부분을 변경할 수 있다.
- 슬라이스한 부분과 대치할 시퀀스의 길이가 다르더라도 적용 가능하다.
찾아봐야하는 내용.
2-12 스트라이드와 슬라이스를 한 식에 함께 사용하지 말라
- 스트라이드(Stride)는
[start:end:stride]
형태로 증가값을 포함하는 슬라이싱 방법이다. - 스트라이드를 사용할 경우 예기치 못한 동작이 발생할 수 있어 주의해야 한다.
- 예를 들어, 문자열을 뒤집을 때는
[::-1]
을 사용한다.
x = b'mongoose'
y = x[::-1]
print(y)
# 출력: b'esognom'
- 유니코드 문자열과 UTF-8로 인코딩된 바이트 문자열은 동작 방식이 다르므로 주의해야 한다.
기억해야 할 내용:
- 슬라이스에 시작, 끝, 증가값을 함께 지정하면 코드의 의미가 혼동될 수 있다.
- 양수 증가값을 사용하라. 가능하면 음수 증가값은 피하라.
- 한 슬라이스에서 시작, 끝, 증가값을 함께 사용하지 말라.
- 대안 1: 두 번의 대입을 사용하여 한 번은 스트라이딩, 한 번은 슬라이싱을 수행하라.
- 대안 2:
itertools.islice()
를 사용하여 더 명확한 코드로 작성하라.
2-13 슬라이싱보다는 나머지를 모두 잡아내는 언패킹을 사용하라
기본 언패킹의 한계
언패킹할 시퀀스의 길이를 미리 알고 있어야 한다.
여러 개의 변수를 인덱싱과 슬라이싱을 통해 할당하면 실수할 가능성이 높다.
oldest = car_ages_descending[0] second_oldest = car_ages_descending[1] others = car_ages_descending[2:] print(oldest, second_oldest, others)
위 코드에서 인덱스 조정을 깜빡하면 잘못된 결과가 나올 수 있다.
해결 방법: `` * (별표) 기호를 사용한 언패킹
oldest, second_oldest, *others = car_ages_descending print(oldest, second_oldest, others)
others
는 나머지 모든 요소를 리스트로 저장한다.- 실수를 줄이고 코드 가독성을 높일 수 있다.
별표(*``) 언패킹의 특징
언패킹할 변수 중 하나 이상은 필수
- 하나만 사용하면
SyntaxError
발생
```python
- 하나만 사용하면
others = car_ages_descending # SyntaxError 발생
한 수준의 언패킹에서 *은 하나만 사용 가능
first, *middle, *last = car_ages_descending # SyntaxError 발생
기억해야 할 내용
- 언패킹 대입에 별표 식을 사용하면 언패킹 패턴에서 대입되지 않는 모든 부분을 리스트에 잡아낼 수 있다.
- 별표 식은 언패킹 패턴의 어떤 위치에든 놓을 수 있다. 별표 식에 대입된 결과는 항상 리스트가 되며, 이 리스트에는 별표 식이 받은 값이 0개 또는 그 이상 들어간다.
- 리스트를 서로 겹치지 않게 여러 조각으로 나눌 경우, 슬라이싱과 인덱싱을 사용하기 보다는 나머지를 모두 잡아내는 언패킹을 사용해야 실수할 여지가 훨씬 줄어든다.
2-14 복잡한 기준을 사용해 정렬할 때는 key 파라미터를 사용하라
- 파이썬의 list 내장 타입에는 리스트 원소를 정렬하는
sort
메서드가 있다. - 기본적으로 원소 타입에 맞춰 오름차순 정렬을 수행한다.
- 사용자가 만든 객체를 정렬하려면 정렬 기준이 될 속성(애트리뷰트)을 key 파라미터로 지정해야 한다.
key 파라미터를 이용한 정렬
names = ['김', '민수', '영희쌤']
# x는 리스트의 각 요소를 의미
# len(x)는 각 요소의 길이를 반환
sorted_names = sorted(names, key=lambda x: len(x))
print(sorted_names) # 출력: ['김', '민수', '영희쌤']
# 실행 과정:
# 1. x = '김' -> len(x) = 1
# 2. x = '민수' -> len(x) = 2
# 3. x = '영희쌤' -> len(x) = 3
- key 함수는 정렬 기준이 될 값을 반환하는 함수이다.
- 위 코드에서는 문자열의 길이를 기준으로 정렬했다.
객체의 특정 속성을 기준으로 정렬
class Car:
def __init__(self, color, brand):
self.color = color
self.brand = brand
my_car = Car("빨강", "현대") # 객체 생성 예제
Car
클래스에는color
와brand
속성이 존재한다.- 정렬 시 특정 속성을 기준으로 정렬할 수 있다.
여러 기준을 활용한 정렬
power_tools.sort(key=lambda x: (x.weight, x.name))
print(power_tools)
# 출력: [Tool('드릴', 4), Tool('연마기', 4), Tool('원형 톱', 5), Tool('착암기', 40)]
(x.weight, x.name)
을 반환하여 무게(weight)를 기준으로 정렬하고, 동일한 무게일 경우 이름(name)으로 정렬한다.
내림차순 정렬
power_tools.sort(key=lambda x: (x.weight, x.name), reverse=True)
print(power_tools)
# 출력: [Tool('착암기', 40), Tool('원형 톱', 5), Tool('연마기', 4), Tool('드릴', 4)]
reverse=True
를 설정하면 모든 정렬 기준이 내림차순이 된다.
여러 번 정렬하여 복합 정렬 적용
power_tools.sort(key=lambda x: x.name) # name 기준 오름차순 정렬
power_tools.sort(key=lambda x: x.weight, reverse=True) # weight 기준 내림차순 정렬
print(power_tools)
# 출력: [Tool('착암기', 40), Tool('원형 톱', 5), Tool('드릴', 4), Tool('연마기', 4)]
- 여러 정렬 기준을 적용할 경우 우선순위가 낮은 기준부터 정렬해야 한다.
- 먼저
name
기준으로 정렬한 후,weight
기준으로 정렬하면 원하는 결과를 얻을 수 있다.
기억해야 할 내용
sort
메서드는 기본적으로 문자열, 정수, 튜플 등 자연스러운 순서가 있는 타입을 정렬할 수 있다.- 사용자가 만든 객체를 정렬하려면 key 파라미터를 사용해 정렬 기준을 지정해야 한다.
- 튜플을 반환하는 key 함수를 사용하면 여러 정렬 기준을 한 번에 적용할 수 있다.
- 정렬 기준이 여러 개일 경우, 낮은 우선순위 기준부터 정렬한 후 높은 우선순위 기준으로 정렬해야 한다.
- 정렬 방향을 반대로 지정하려면
reverse=True
를 사용하거나, 부호를 바꿀 수 있는 값이면 - 연산자를 사용하면 된다.
2-15 딕셔너리 삽입 순서에 의존할 때는 조심하라
- 파이썬 3.5 이전: 딕셔너리의 삽입 순서가 보장되지 않았다.
- 파이썬 3.7 이후: 딕셔너리의 삽입 순서가 보장됨. 즉,
dict
를 이터레이션할 때 삽입한 순서대로 키를 반환한다. - 하지만, 모든 딕셔너리와 비슷한 객체가 삽입 순서를 보장한다고 가정하면 안 된다.
딕셔너리 삽입 순서 보장 예제
my_dict = {'a': 1, 'b': 2, 'c': 3}
for key in my_dict:
print(key)
# 출력: a, b, c (삽입한 순서대로 출력됨)
딕셔너리와 유사한 객체에서의 주의점
파이썬에서는 dict
가 아니지만 딕셔너리처럼 동작하는 클래스를 쉽게 만들 수 있다.
이 경우, 덕 타이핑(Duck Typing)으로 인해 예상치 못한 동작이 발생할 수 있으며, 특히 삽입 순서가 유지되지 않을 수도 있음을 유의해야 한다.
유사 딕셔너리
유사 딕셔너리란?
딕셔너리처럼 동작하지만 실제 dict가 아닌 객체를 말한다.
일반 딕셔너리와 다르게 동작할 수 있어 주의해야 한다.
예제
defaultdict → 없는 키를 조회하면 기본값을 자동으로 반환
직접 만든 FakeDict → 내부 동작을 마음대로 변경 가능 (예: 키 순서 변경)
os.environ → 환경 변수를 저장하지만 실제 dict가 아님
주의할 점
딕셔너리와 다르게 동작할 수 있으므로 조심해야 한다.
isinstance(obj, dict)로 실제 딕셔너리인지 확인하는 것이 좋다.
이해하기 쉬운 예제*
from collections import defaultdict # 일반 딕셔너리 normal_dict = {} # 없는 키를 조회하면 오류 발생 # print(normal_dict['a']) # KeyError # 유사 딕셔너리 (defaultdict) default_dict = defaultdict(int) print(default_dict['a']) # 0 (오류 없이 기본값 반환)
→ 일반 딕셔너리는 없는 키를 조회하면 오류가 나지만, defaultdict는 기본값을 반환한다.
→ 이런 차이 때문에 딕셔너리처럼 보이지만 조심해야 한다.
덕 타이핑으로 인한 오류 예제
class FakeDict:
def __init__(self):
self.data = {}
def __setitem__(self, key, value):
self.data[key] = value
def __getitem__(self, key):
return self.data[key]
def keys(self):
return reversed(self.data.keys()) # 일부러 순서를 바꿔서 반환
my_dict = FakeDict()
my_dict['a'] = 1
my_dict['b'] = 2
my_dict['c'] = 3
print(list(my_dict.keys()))
# 출력: ['c', 'b', 'a'] (순서가 변경됨)
FakeDict
는dict
처럼 보이지만 실제dict
가 아니므로, 키 삽입 순서가 보장되지 않는다.keys()
메서드에서 순서를 의도적으로 뒤집으면,dict
처럼 사용하던 코드가 예상과 다르게 동작할 수 있다.- 이런 문제가 발생하는 이유는 덕 타이핑(Duck Typing) 방식으로 인해,
dict
와 호환될 것이라고 가정하고 사용했기 때문이다.
딕셔너리를 조심스럽게 다루는 방법
삽입 순서 보존에 의존하지 않는 코드 작성
- 딕셔너리를 사용할 때 순서가 중요한 경우
OrderedDict
를 고려하는 것이 좋다.
- 딕셔너리를 사용할 때 순서가 중요한 경우
실행 시점에
dict
타입 검사if isinstance(obj, dict): print("이 객체는 딕셔너리입니다.")
타입 애너테이션과 정적 분석 사용 (
mypy
등)def process_data(data: dict[str, int]) -> None: pass
dict[str, int]
타입을 명시하면, 다른 딕셔너리 유사 객체가 들어오는 경우 정적 분석에서 감지할 수 있다.
기억해야 할 내용
- 파이썬 3.7부터는
dict
의 삽입 순서가 유지되므로, 이를 활용할 수 있다. - 그러나 딕셔너리와 비슷한 객체들은 삽입 순서를 유지하지 않을 수도 있으므로 주의해야 한다.
- 딕셔너리와 유사한 클래스를 안전하게 다루려면
- 삽입 순서에 의존하지 않는 코드 작성
- 실행 시점에 명시적으로
dict
타입을 검사 - 타입 애너테이션과 정적 분석을 활용
2-16 in
을 사용하고 KeyError
를 처리하기보다는 get
을 사용하라
- 딕셔너리의 기본 연산:
- 키에 접근 (
dict[key]
) - 키에 값 대입 (
dict[key] = value
) - 키 삭제 (
del dict[key]
)
- 키에 접근 (
- 딕셔너리는 동적 데이터 구조이므로, 특정 키가 존재하지 않을 가능성을 항상 고려해야 한다.
get()
메서드란?
get()
메서드는 딕셔너리에서 키를 찾을 수 없을 때 기본값을 반환하는 메서드이다.
예제: 일반적인 딕셔너리 접근 방식 (KeyError 발생 가능)
student_scores = {'Kim': 85, 'Lee': 90}
# 존재하지 않는 키에 접근 (KeyError 발생)
score = student_scores['Park'] # KeyError!
get()
을 사용하면 안전하게 처리 가능
# 'Park'이 존재하지 않으면 기본값 0을 반환
score = student_scores.get('Park', 0)
print(score) # 출력: 0
get()
메서드 구조
dictionary.get(key, default_value)
- key: 찾고자 하는 키
- default_value: 키가 없을 때 반환할 기본값 (생략 시
None
반환)
KeyError
예외를 사용하는 방식과 비교
1. KeyError
를 예외로 처리하는 방식 (비효율적)
try:
count = counters[key]
except KeyError:
count = 0
counters[key] = count + 1
- 단점:
KeyError
예외가 발생하면 코드 실행 속도가 느려지고, 코드가 길어진다.
2. get()
을 사용한 방식 (더 간결하고 빠름)
count = counters.get(key, 0)
counters[key] = count + 1
- 장점: 키를 한 번만 읽고, 한 번만 대입하므로 코드가 더 짧고 효율적이다.
- 카운터와 같은 단순한 데이터 처리에는
get()
이 가장 적합하다.
추가적인 딕셔너리 키 처리 방법
1. in
을 사용한 방식
if key in counters:
count = counters[key]
else:
count = 0
counters[key] = count + 1
- 단점:
in
을 사용하면 키를 두 번 검사해야 하므로 비효율적이다.
2. setdefault()
를 사용한 방식
counters.setdefault(key, 0)
counters[key] += 1
- 주의점:
setdefault()
는 값을 설정할 때 불필요한 연산이 발생할 수도 있다. - 대안: 복잡한 값(리스트, 딕셔너리 등)을 기본값으로 설정해야 할 경우,
defaultdict
를 고려하는 것이 좋다.
3. defaultdict
를 사용한 방식 (추천)
from collections import defaultdict
counters = defaultdict(int) # 기본값이 0인 딕셔너리 생성
counters[key] += 1
- 장점: 키가 존재하지 않으면 자동으로 기본값(0) 설정.
setdefault()
보다 더 깔끔하고 효율적이다.
기억해야 할 내용
- 딕셔너리 키가 없는 경우를 처리하는 방법:
in
을 사용하여 검사KeyError
예외 처리 (try-except
)get()
사용 (간단한 경우에 가장 적합)setdefault()
사용 (defaultdict
가 더 나을 수도 있음)
- 카운터 같은 기본 데이터 처리에는
get()
이 가장 효율적이고 간결한 방법이다. - 딕셔너리에 넣을 값을 생성하는 비용이 크거나 예외가 발생할 가능성이 있으면
get()
이 더 적절하다. setdefault()
를 사용해야 한다면,defaultdict
를 고려하는 것이 더 나은 선택일 수 있다.
2-17 내부 상태에서 원소가 없는 경우를 처리할 때는 setdefault
보다 defaultdict
를 사용하라
- 직접 생성하지 않은 딕셔너리를 다룰 때, 키가 존재하지 않는 경우를 처리하는 방법이 필요하다.
get()
을 사용하는 것이in
과KeyError
보다 낫지만, 특정 상황에서는setdefault()
가 더 빠를 수도 있다.- 그러나
defaultdict
를 사용하면 더욱 깔끔하고 효율적인 코드 작성이 가능하다.
setdefault()
vs defaultdict
1. setdefault()
사용 예제
data = {}
# 키가 존재하지 않으면 기본값으로 빈 리스트를 설정
data.setdefault('key1', []).append(10)
data.setdefault('key2', []).append(20)
print(data)
# 출력: {'key1': [10], 'key2': [20]}
setdefault()
는 키가 존재하지 않으면 기본값을 설정한 후 값을 추가한다.- 단점: 매번
setdefault()
를 호출해야 하므로 코드가 장황해질 수 있다.
2. defaultdict
사용 예제 (추천)
from collections import defaultdict
data = defaultdict(list) # 기본값이 리스트인 defaultdict 생성
data['key1'].append(10)
data['key2'].append(20)
print(data)
# 출력: {'key1': [10], 'key2': [20]}
# 디폴트를 5로
my_dict = defaultdict(lambda: 5)
print(my_dict['a']) # 존재하지 않는 키 접근 -> 5 출력
print(my_dict['b']) # 존재하지 않는 키 접근 -> 5 출력
print(dict(my_dict)) # {'a': 5, 'b': 5}
defaultdict
는 키가 없을 경우 자동으로 기본값을 생성해주므로,setdefault()
보다 코드가 더 간결해진다.- 장점:
setdefault()
보다 더 직관적이고 사용하기 쉽다.
setdefault()
와 defaultdict
의 차이점
비교 항목 | setdefault() |
defaultdict |
---|---|---|
키가 없을 때 | 기본값을 설정한 후 값 추가 | 기본값을 자동으로 설정 |
코드 가독성 | 반복적인 setdefault() 호출 필요 |
더 깔끔하고 직관적 |
성능 | setdefault() 호출 시마다 딕셔너리를 수정 |
기본값이 자동 생성되므로 더 빠름 |
사용 예 | 특정 경우 (get() 보다 짧은 코드가 필요할 때) |
키가 동적으로 추가되는 경우에 적합 |
기억해야 할 내용
- 키가 동적으로 추가되는 딕셔너리를 다룰 때는
defaultdict
를 사용하는 것이 가장 효율적이다. - 직접 생성하지 않은 딕셔너리를 다룰 때는
get()
을 우선적으로 사용해야 한다. - 코드가
setdefault()
를 사용하면 더 짧아지는 경우에는setdefault()
도 고려할 수 있다. - 하지만 대부분의 경우
defaultdict
가 더 간결하고 직관적인 해결책이다.
2-18 __missing__
을 사용해 키에 따라 다른 디폴트 값을 생성하는 방법을 알아두라
dict
타입을 상속받아__missing__
특별 메서드를 구현하면 키가 없는 경우의 처리 로직을 직접 커스텀할 수 있다.- 일반적인
defaultdict
는 키 정보를 알 수 없지만,__missing__
메서드는 키를 활용해 디폴트 값을 생성할 수 있다.
__missing_ 메서드 정리
missing이란?
missing은 딕셔너리에서 없는 키를 조회할 때 자동으로 호출되는 메서드
즉, 딕셔너리에서 키가 없을 때 어떻게 처리할지 직접 정의할 수 있게 해주는 기능.더 디테일 하게
__missing__
이란?딕셔너리에서 없는 키를 조회할 때 자동으로 호출되는 메서드다.
기본값을 동적으로 생성할 수 있어
defaultdict
보다 유연하게 사용할 수 있다.
1. 기본 사용법
class MyDict(dict): def __missing__(self, key): return f"'{key}' 키가 없습니다." d = MyDict() print(d['test']) # "'test' 키가 없습니다."
키가 없을 때
__missing__
이 호출되어 원하는 값이 반환된다.
2.
defaultdict
와의 차이from collections import defaultdict d = defaultdict(lambda: "기본값") print(d['hello']) # "기본값" print(d['world']) # "기본값"
defaultdict
는 모든 없는 키에 대해 같은 기본값을 반환한다.class MyDict(dict): def __missing__(self, key): return f"'{key}' 키의 길이는 {len(key)}입니다." d = MyDict() print(d['hello']) # "'hello' 키의 길이는 5입니다." print(d['python']) # "'python' 키의 길이는 6입니다."
__missing__
은 키별로 다른 기본값을 설정할 수 있다.
3.
defaultdict
vs__missing__
기능 defaultdict
__missing__
없는 키 처리 같은 기본값 반환 키별로 다른 값 반환 가능 설정 방식 생성 시 함수 지정 __missing__
메서드에서 정의유연성 제한적 유연함 기본값이 항상 같다면
defaultdict
를, 키에 따라 다른 값이 필요하다면__missing__
을 사용하면 된다.
개념
- dict 클래스의 하위 클래스에서 사용하는 특별 메서드
- 딕셔너리에서 없는 키에 접근할 때 자동으로 호출됨
특징
- 키가 없을 때의 동작을 직접 정의 가능
- 키 값에 따라 다른 기본값 생성 가능
- defaultdict보다 더 유연한 기본값 처리 가능
기본 사용법
class MyDict(dict):
def __missing__(self, key):
value = f"키 '{key}'에 대한 기본값"
self[key] = value
return value
d = MyDict()
print(d['test']) # 키 'test'에 대한 기본값
defaultdict와의 차이
- defaultdict: 모든 없는 키에 대해 동일한 타입의 기본값 생성
- missing_: 키별로 다른 기본값을 동적으로 생성 가능
defaultdict
의 한계
from collections import defaultdict
def default_factory():
return "기본값"
data = defaultdict(default_factory)
print(data['없는 키']) # 출력: "기본값"
defaultdict
는 키가 없을 때 자동으로 하나의 공통된 값을 반환하지만, 키에 따라 다른 디폴트 값을 생성할 수는 없다.
__missing__
을 사용한 dict
하위 클래스 예제
class CustomDict(dict):
def __missing__(self, key):
return f"{key}는 존재하지 않습니다."
data = CustomDict()
print(data['없는 키'])
# 출력: "없는 키는 존재하지 않습니다."
__missing__
메서드는 키가 없을 때 호출되며, 키에 따라 커스텀 메시지를 반환할 수 있다.defaultdict
와 달리, 사용자가 입력한 키 값을 활용하여 디폴트 값을 생성할 수 있다.
__missing__
을 활용한 디폴트 값 생성 예제
class CountDict(dict):
def __missing__(self, key):
return len(key) # 키의 길이를 디폴트 값으로 반환
data = CountDict()
print(data['hello']) # 출력: 5
print(data['python']) # 출력: 6
__missing__
을 이용하면 키 자체를 활용하여 디폴트 값을 동적으로 생성할 수 있다.- 위 예제에서는 키의 길이를 디폴트 값으로 반환하도록 구현했다.
기억해야 할 내용
- 디폴트 값을 생성하는 비용이 높거나 예외가 발생할 가능성이 있다면
setdefault()
를 사용하지 않는 것이 좋다. defaultdict
는 함수에 인자를 전달할 수 없으므로, 키에 따라 다른 디폴트 값을 생성할 수 없다.- 키 정보를 활용한 디폴트 값이 필요하면
dict
의 하위 클래스를 만들고__missing__
메서드를 직접 정의하면 된다.
'리뷰 > 책' 카테고리의 다른 글
[파이썬 코딩의 기술] - 3장 함수 (0) | 2025.02.14 |
---|---|
[파이썬 코딩의 기술] - 1장 파이썬답게 생각하기 (0) | 2025.02.14 |
Do it! 첫 코딩 - 5장 (0) | 2025.02.14 |
Do it! 첫 코딩 - 4장 (0) | 2025.02.14 |
Do it! 첫 코딩 - 3장 (0) | 2025.02.14 |