Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

[11주차 2] Effective Python: Chapter 2. Lists and Dictionaries #124

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
369 changes: 369 additions & 0 deletions hong/python/effective-python/2. Lists and Dictionaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
# 2장 리스트와 딕셔너리

- `list`: 많은 프로그램이 사람보다 기계가 하기에 더 적합한 반복적인 작업을 자동화 하고자 만들어졌다. 파이썬에서 이런 작업을 조직적으로 관리하는 가장 일반적인 방법은 리스트(list) 타입을 쓰는 것이다. 리스트는 아주 간편하며 다양한 문제를 해결하는데 사용할 수 있다.
- `dict`: 리스트를 자연스럽게 보완할 수 있는 타입이 딕셔너리(dict) 타입이다. 딕셔너리 타입은 검색에 사용할 키와 키에 연관된 값을 저장한다(일반적으로 해시테이블이나 연관 배열이라고 부르는 데이터 구조 안에 값을 저장한다). 딕셔너리는 상수 시간에 원소를 삽입하고 검색할 수 있기 때문에, 동적인 정보를 관리하는 목적으로 가장 이상적이다.

## Better Way 11: 시퀀스를 슬라이싱하는 방법을 익혀라

- 슬라이싱(slicing)을 사용하면 시퀀스에 들어 있는 아이템의 부분집합에 쉽게 접근할 수 있다.
- 슬라이싱의 기본 형태는 `list[start:end]` 이다.
- `end` 인덱스에 있는 원소는 포함되지 않는다.
- 리스트의 맨 앞부터 슬라이싱할 때는 기본적으로 0을 생략한다.
- 리스트를 끝까지 슬라이싱할 때도 마지막 len를 생략한다.
- 리스트의 끝에서부터 원소를 찾고 싶을 때는 음수 인덱스를 사용한다.

```python
# [0] [1] [2] [3]
a = ['a', 'b', 'c', 'd']
# [-4] [-3] [-2] [-1]

assert a[:3] == a[0:4]
assert a[4:] == a[4:len(a)]
```

- 슬라이싱은 범위를 넘어가는 시작 인텍스나 끝 인덱스도 허용한다.

```python
a[:20]
# Output: ['a', 'b', 'c', 'd']
```

- 리스트에 지정한 슬라이스 길이보다 대입되는 배열의 길이가 더 길면 리스트가 늘어난다.
- 반대로 리스트에 지정한 슬라이스 길이보다 대입되는 배열의 길이가 더 족으면 리스트도 줄어든다.

```python
a[2:3] = ['e', 'f'] # 'c'를 'e', 'f'로 변경
a
# ['a', 'b', 'e', 'f', 'd']
```

### 기억해야 할 내용

- 슬라이싱할 때는 간결하게 하라. 시작 인덱스에 0을 넣거나, 끝 인덱스에 시퀀스 길이를 넣지 말라.
- 슬라이싱은 범위를 넘어가는 시작 인덱스나 끝 인덱스도 허용한다. 따라서 시퀀스의 시작이나 끝에서 길이를 제한하는 슬라이스(`a[:20]` 이나 `a[-20:]`)를 쉽게 표현할 수 있다.
- 리스트 슬라이스에 대입하면 원래 시퀀스에서 슬라이스가 가리키는 부분을 대입 연산자 오른쪽에 있는 시퀀스로 대치한다. 이때 슬라이스와 대치되는 시퀀스의 길이가 달라도 된다.

## Better Way 12: 스트라이드와 슬라이스를 한 식에 함께 사용하지 말라

- `list[start:end:stride]`으로 일정한 간격을 두고 슬라이싱할 수 있다.
- 이를 스트라이드(stride, 보폭) 이라고 한다.

```python
a = ['a', 'b', 'c', 'd']
print(a[::2]) # Output: ['a','c']
print(a[1::2]) # Output: ['b', 'd']
```

- 하지만, 슬라이딩 구문에 스트라이딩까지 들어가면 혼란스러울 수 있다.
- 따라서 시작값이나 끝값을 증가값과 함께 사용하지 않는다.
- 증가값을 사용해야 하는 경우에는 양수값으로 만들고 시작과 끝 인덱스를 생략한다.
- 만약, 세 파라미터를 모두 써야하는 경우, 두 번 대입을 사용하는 방식을 적용한다. (한 번은 스트라이딩, 한 번은 슬라이싱)

```python
a = ['a', 'b', 'c', 'd']
y = a[::2]
z = y[:-1]
z # Output: ['a']
```

### 기억해야 할 내용

- 슬라이스에 시작, 끝, 증가값을 함께 지정하면 코드의 의미를 혼동하기 쉽다.
- 시작이나 끝 인덱스가 없는 스라이스를 만들 때는 양수 증가값을 사용하라. 가급적 음수 증가값은 피하라.
- 한 스라이스 안에서 시작, 끝, 증가값을 함께 사용하지 말라. 세 파라미터를 모두 써야 하는 경우, 두 번 대입을 사용(한 번은 스트라이딩, 한 번은 슬라이싱)하거나 `itertools` 내장 모듈의 `islice`를 사용하라.

## Better way 13: 슬라이싱보다는 나머지를 모두 잡아내는 언패킹을 사용하라

- 파이썬은 별표 식(starred expression)을 사용해 모든 값을 담는 언패킹을 할 수 있게 지원한다.
- 이 구문을 사용하면 언패킹 패턴의 다른 부분에 들어가지 못하는 모든 값을 별이 붙은 부분에 다 담는다.
- 별표 식은 항상 list 인스턴스가 된다.

```python
car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
sorted_car_ages = sorted(car_ages, reverse=True)

first, second = sorted_car_ages # Oputput: ValueError: too many values to unpack

first, second, *others = sorted_car_ages
print(first, second, others)
# 20 19 [15, 9, 8, 7, 6, 4, 1, 0]
```

- 주의:
- 별표 식이 포함된 언패킹 대입을 처리하려면 필수인 부분이 적어도 하나는 있어야 한다.
- 또한, 한 수준의 언패킹 패턴에 별표 식을 두 개 이상 쓸 수 없다.
- 하지만, 여러 계층으로 이뤄진 구조를 언패킹할 때는 서로 다른 부분에 포함되는 한, 별표 식을 여러개 사용할 수 있다.

### 기억해야 할 내용

- 언패킹 대입에 별표 식을 사용하면 언패킹 패턴에서 대입되지 않는 모든 부분을 리스트에 잡아낼 수 있다.
- 별표 식은 언패킹 패턴의 어떤 위치에든 놓을 수 있다. 별표 식에 대입된 결과는 항상 리스트가 되며, 이 리스트에는 별표 식이 받은 값이 0개 또는 그 이상 들어간다.
- 리스트를 서로 겹치지 않게 여러 조각으로 나눌 경우, 슬라이싱과 인덱싱을 사용하기보다는 나머지를 모두 잡아내는 언패킹을 사용해야 실수할 여지가 훨씬 줄어든다.

## Better Way 14: 복잡한 기준을 사용해 정렬할 때는 key 파라미터를 사용하라

- sort는 list의 메소드로 리스트의 내용을 원소 타입에 따른 자연스러운 순서를 사용해 오름차순으로 정렬한다.

```python
numbers = [93, 86, 11, 68, 70]
numbers.sort()
print(numbers) # [11, 68,70, 86, 93]
```

- `<list>.sort(key = <function>, reverse = <bool>)`
- `sort` 메서드는 자연스럽게 순서를 정할 수 있는 거의 대부분의 내장 타입(문자열, 부동소수점 수 등)에 대해 잘 작동한다.
- 하지만 객체를 대상으로 sort를 사용할 경우, sort 메서드가 호출하는 객체 비교 특별 메서드가 정의되어 있지 않으므로 아래와 같은 타입에러가 발생한다.

```python
class Tool:
def __init__(self, name, weight):
self.name = name
self.weight = weight

def __repr__(self):
return f'Tool({self.name!r}, {self.weight})'

tools = [
Tool('수준계', 3.5),
Tool('해머', 1.25),
Tool('스크류드라이버', 0.5),
Tool('끌', 0.25),
]

tools.sort()
# Output:
# Traceback ...
# TypeError: '<' not supported between instances of 'Tool' and 'Tool'
```

- 정렬에 사용하고 싶은 attribute가 객체에 들어 있는 겅우, 이런 상황을 지원하기 위해 sort 메소드에는 key라는 파라미터가 있다. key 파라미터에 대한 설명은 아래와 같다.
- key는 함수여야 한다.
- key 함수에는 정렬 중인 리스트의 원소가 전달된다.
- key 함수가 반환하는 값은 원소 대신 정렬 기준으로 사용할, 비교 가능한 값이어야만 한다.

```python
# lambda 키워드로 정의된 함수를 사용한 sorting
print('미정렬:', repr(tools))
tools.sort(key=lambda x: x.name)
print('\n정렬:', tools)
# Output
# 미정렬: [Tool('수준계', 3.5), Tool('해머', 1.25), Tool('스크류드라이버', 0.5), Tool('끌', 0.25)]
# 정렬: [Tool('끌', 0.25), Tool('수준계', 3.5), Tool('스크류드라이버', 0.5), Tool('해머', 1.25)]
```

### 기억해야 할 내용

- 리스트 타입에 들어 있는 sort 메서드를 사용하면 원소 타입이 문자열, 정수, 튜플 등과 같은 내장 타입인 경우 자연스러운 순서로 리스트의 원소를 정렬할 수 있다.
- 원소 타입에 특별 메서드를 통해 자연스러운 순서가 정의돼 있지 않으면 sort 메서드를 쓸 수 없다. 하지만 원소 타입에 순서 특별 메서드를 정의하는 경우는 드물다.
- sort 메서드의 key 파라미터를 사용하면 리스트의 각 원소 대신 비교에 사용할 객체를 반환하는 도우미 함수를 제공할 수 있다.
- key 함수에서 튜플을 반환하면 여러 정렬 기준을 하나로 엮을 수 있다. 단항 부호 반전 연산자를 사용하면 부호를 바꿀 수 있는 타입이 정렬 기준인 경우 정렬 순서를 반대로 바꿀 수 있다.
- 부호를 바꿀 수 없는 타입의 경우 여러 정렬 기준을 조합하려면 각 정렬 기준마다 reverse 값으로 정렬 순서를 지정하면서 sort 메서드를 여러 번 사용해야 한다. 이때 정렬 기준의 우선순위가 점점 높아지는 순서로 sort를 호출해야 한다.

## Better Way 15: 딕셔너리 삽입 순서에 의존할 때는 조심하라

- 파이썬 3.6부터는 딕셔너리가 삽입 순서를 보존하도록 동작이 개선됐다.

```python
baby_names = {
'cat': 'kitten',
'dog': 'puppy',
}
print(baby_names) # {'cat': 'kitten', 'dog': 'puppy'}
print(list(baby_names.keys()) # ['cat', 'dog']
print(list(baby_names.values()) # ['kitten', 'puppy']
print(list(baby_names.items()) # [('cat', 'kitten'), ('dog', 'puppy')]
print(baby_names.popitem()) # ('dog', 'puppy')
print(baby_names) # {'cat': 'kitten'}
```

- 키워드 인자의 순서는 함수를 호출할 때 사용한 인자 순서와 일치한다.

```python
def my_func(**kwargs):
for key, value in kwargs.items():
print(f'{key} = {value}')

my_func(goose='gosling', kangaroo='joey')
# Output:
# goose = gosling
# kangaroo = joey
```

### 기억해야 할 내용

- 파이썬 3.7부터는 dict 인스턴스에 들어 있는 내용을 이터레이션할 때 키를 삽입한 순서대로 돌려받는다는 사실에 의존할 수 있다.
- 파이썬은 dict는 아니지만 딕셔너리와 비슷한 객체를 쉽게 만들 수 있게 해준다. 이런 타입의 경우 키 삽입 순서가 그대로 보존된다고 가정할 수 없다.
- 딕셔너리와 비슷한 클래스를 조심스럽게 다루는 방법으로는 dict 인스턴스의 삽입 순서 보존에 의존하지 않고 코드를 작성하는 방법, 실행 시점에 명시적으로 dict 타입을 검사하는 방법, 타입 애너테이션과 정적 분석을 사용해 dict 값을 요구하는 방법이 있다.

## Better way 16: in을 사용하고 딕셔너리 키가 없을 때 KeyError를 처리하기보다는 get을 사용하라

- 딕셔너리와 상호작용하는 세 가지 기본 연산은 키나 키에 연관된 값에 접근하고, 대입하고, 삭제하는 것이다.
- 다음은 if 문과 키가 존재할 때 true를 반환하는 in과 존재하지 않는 키에 접근할 때 발생시키는 KeyError 예외를 사용하는 코드이다.

```python
counters = {
'폼퍼니켈': 2,
'사워도우': 1,
}

key = '밀'

# in 사용 예제
if key in counters:
count = counters[key]
else:
count = 0

counters[key] = count + 1

counters # Output: {'품퍼니켈': 2, '사워도우': 1, '밀': 1}

# KeyError 사용 예제
try:
count = counters[key]
except KeyError:
count = 0

counters[key] = count + 1 # Output: {'품퍼니켈': 2, '사워도우': 1, '밀': 2}
```

- 다음은 dict 내장 타입의 get 메서드를 활용한 방법이다. (추천하는 방식)

```python
counters = {
'폼퍼니켈': 2,
'사워도우': 1,
}

key = '밀'

count = counters.get(key, 0)
counters[key] = count + 1

counters # Output: {'품퍼니켈': 2, '사워도우': 1, '밀': 1}
```

### 기억해야 할 내용

- 딕셔너리 키가 없는 경우를 처리하는 방법으로는 in 식을 사용하는 방법, KeyError 예외를 사용하는 방법, get 메서드를 사용하는 방법, setdefault를 사용하는 방법이 있다.
- 카운터와 같이 기본적인 타입의 값이 들어가는 딕셔너리를 다룰 때는 get 베서드가 가장 좋고, 딕셔너리에 넣을 값을 만드는 비용이 비싸거나 만드는 과정에 예외가 발생할 수 있는 경우에도 get 메서드를 사용하는 편이 더 낫다.
- 해결하려는 문제에 dict의 setdefault 메서드를 사용하는 방법이 가장 적합해 보인다면 setdefault 대신 defaultdict를 사용할지 고려해보라 (아래에서 더 설명)

## Better way 17: 내부 상태에서 원소가 없는 경우를 처리할 때는 setdefault보다 defaultdict를 사용하라

- 직접 만들지 않은 딕셔너리를 다룰 때, 키가 없는 경우를 처리하는 방법 중 경우에 따라서는 setdefault가 가장 빠른 지름길일 수 있다.

```python
visits = {
'미국': {'뉴욕', '로스엔젤로스'},
'일본': {'하코네'}
}

# 딕셔너리 안에 나라 이름이 들어 있는지 여부와 관계없이
# 각 집합에 새 도시를 추가할 때 setdefault를 사용할 수 있다.
visits.setdefault('프랑스', set()).add('칸')

print(visits) # Output: {'미국': {'로스엔젤로스', '뉴욕'}, '일본': {'하코네'}, '프랑스': {'칸'}}
```

- collections 내장 모듈에 있는 defaultdict 클래스는 키가 없을 때 자동으로 디폴트 값을 지정해준다.

```python
from collections import defaultdict

class Visits():
def __init__(self):
self.data = defaultdict(set)

def add(self, country, city):
self.data[country].add(city)

visits = Visits()
visits.add('영국', '바스')
visits.add('영국', '런던')
print(visits.data) # Output: defaultdict(<class 'set'>, {'영국': {'런던', '바스'}})
```

- 이렇게 add 메서드의 구현을 더 짧고 간단하게 만들 수 있다.
- add 코드는 data 딕셔너리에 있는 키에 접근하면 항상 기존 set 인스턴스가 반환된다고 가정한다.
- add 메서드가 아주 많이 호출되면 집한 생성에 따른 비용도 커지는데, 이 구현에서는 불필요한 set이 만들어지는 경우는 없다.

### 기억해야 할 내용

- 키로 어떤 값이 들어올지 모르는 딕셔너리를 관리해야 하는데 collections 내장 모듈에 있는 defaultdict 인스턴스가 여러분의 필요에 맞아떨어진다면 defaultdict를 사용하라.
- 임의의 키가 들어 있는 딕셔너리가 여러분에게 전달됐고 그 딕셔너리가 어떻게 생성됐는지 모르는 경우, 딕셔너리의 원소에 접근하려면 우선 get을 사용해야 한다. 하지만 setdefault가 더 짧은 코드를 만들어내는 몇 가지 경우에는 setdefault를 사용하는 것도 고려해볼 만하다.

## Better way 18: __missing__ 을 사용해 키에 따라 다른 디폴트 값을 생성하는 방법을 알아두라

- 예를 들어, 파일 시스템에 있는 SNS 프로필 사진을 관리하는 프로그램을 작성한다고 가정해본다.
- 그럼 필요할 때 파일을 읽고 쓰기 위해 프로필 사진의 경로와 열린 파일 핸들을 연관시켜주는 딕셔너리가 필요하다.
- 아래 예제는 일반 dict 인스턴스를 사용하고 get 메서드와 대입식을 통해 키가 딕셔너리에 있는지 검사한다.

```python
pictures = {}
path = 'profile_1234.png'

if (handle := pictures.get(path)) is None:
try:
handle = open(path, 'a+b')
except OSError:
print(f'경로를 열 수 없습니다: {path}')
raise
else:
pictures[path] = handle

handle.seek(0)
image_data = handle.read()
```

- 내부 상태를 관리하려 한다면 프로필 사진의 상태를 관리하기 위해 defaultdict를 쓸 수도 있다고 가정할 수도 있다.
- 다음 코드는 이전 예제와 똑같은 로직을 도우미 함수와 defaultdict 클래스를 사용해 작성한 것이다.

```python
from collections import defaultdict

def open_picture(profile_path):
try:
return open(profile_path, 'a+b')
except OSError:
print(f'경로를 열 수 없습니다: {profile_path}')
raise

pictures = defaultdict(open_picture)
handle = pictures[path] # 불가능한 방법
handle.seek(0)
image_data = handle.read()
# Output:
# Traceback ...
# TypeError: open_picture() missing 1 requred posisional argument: 'profile_path'
```

- 위의 문제는 defaultdict 생성자에 전달한 함수는 인수를 받을 수 없다는 데 있다.
- 이는 defaultdict가 호출하는 도우미 함수가 처리 중인 키를 알 수 없다는 뜻이다.
- 이로 인해 파일 경로를 사용해 open을 호출할 방법이 없다. 이런 상황에서는 setdefault와 defaultdict 모두 필요한 기능을 제공하지 못한다.

- 이런 상황이 흔히 발생하기 때문에 파이썬은 다른 해법을 내장해 제공한다.
- dict 타입의 하위 클래스를 만들고 `__missing__` 특별 메서드를 구현하면 키가 없는 경우를 처리하는 로직을 커스텀화 할 수 있다.
- 다음 코드는 앞의 예제와 똑같은 open_picture 도우미 함수를 활용하는 새로운 크래스를 정의해서 카가 없는 경우 파일을 여는 딕셔너리를 만든다.

```python
class Pictures(dict):
def __missing__(self, key):
value = open_picture(key)
self[key] = value
return value

pictures = Pictures()
handle = pictures[path]
handle.seek(0)
image_data = handle.read()
```

- pictures[path]라는 딕셔너리 접근에서 path가 딕셔너리에 없으면 __missing__ 메서드가 호출된다. 이 메서드는 키에 해당하는 디폴트 값을 생성해 딕셔너리에 넣어준 다음에 호출한 쪽에 그 값을 반환해야 한다.
- 그 이후 딕셔너리에서 같은 경로에 접근하면 이미 해당 원소가 딕셔너리에 들어 있으므로 __missing__ 이 호출되지 않는다.

### 기억해야 할 내용

- 디폴트 값을 만드는 계산 비용이 높거나 만드는 과정에서 예외가 발생할 수 있는 상황에서는 dict의 setdefault 메서드를 사용하지 말라.
- defaultdict에 전달되는 함수는 인자를 받지 않는다. 따라서 접근에 사용한 키 값에 맞는 디폴트 값을 생성하는 것은 불가능하다.
- 디폴트 키를 만들 때 어떤 키를 사용했는지 반드시 알아야 하는 상황이라면 직접 dict의 하위 클래스와 __missing__ 메서드를 정의하면 된다.
Loading