Skip to content

Commit

Permalink
Create 4. Comprehensions and Generators.md (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
deepbig authored Dec 16, 2024
1 parent 35f0836 commit e729296
Showing 1 changed file with 212 additions and 0 deletions.
212 changes: 212 additions & 0 deletions hong/python/effective-python/4. Comprehensions and Generators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Chapter 4. Comprehensions and Generators

### 컴프리헨션(Comprehension) (Better Way 27-29):

- 많은 프로그램이 리스트 딕셔너리의 키/값 쌍, 집합 처리를 중심으로 만들어진다.
- 파이썬에서는 컴프리헨션이라는 특별한 구문을 사용해 이런 (리스트, 딕셔너리, 집합 등) 타입을 간결하게 이터레이션하면서 원소로부터 파생되는 데이터 구조를 생성할 수 있다.
- 컴프리헨션을 사용하면 이런 타입에 대해 일반적인 작엄을 수행하는 코드의 가독성을 높일 수 있고 몇 가지 다른 이점도 얻을 수 있다.

### 제너레이터(Generator) (Better Way 30-35):

- 컴프리헨션 코딩 스타일은 제너레이터를 사용한느 함수로 확장할 수 있다.
- 제너레이터는 함수가 점진적으로 반환하는 값으로 이뤄지는 스트림을 만들어준다.
- 이터레이터를 사용할 수 있는 곳 (for 루프, 별표 식 등)이라면 어디에서나 제너레이터 함수를 호출한 결과를 사용할 수 있다.
- **제너레이터를 사용하면 성능을 향상시키고, 메모리 사용을 줄이고, 가독성을 높일 수 있다.**

## Better Way 27: map과 filter 대신 컴프리헨션을 사용하라

```python
# map과 filter 사용
a = [1,2,3,4,5,6,7,8,9,10]
alt_dict =dict(map(lambda x: (x, x**2), filter(lambda x: x % 2 == 0, a)))
alt_set = set(map(lambda x: x**3, filter(lambda x: x % 3 == 0, a)))

# 리스트 컨프리헨션
even_squares = [x**2 for x in a if x % 2 == 0]
# 딕셔너리 컴프리헨션
even_squares_dict = {x: x**2 for x in a if x % 2 == 0}
print(even_squares_dict) # Output: {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}
# 집합 컴프리헨션
three_cubed_set = {x**3 for x in a if x % 3 == 0}
print(threes_cubed_set) # Output: {216, 729, 27}
```

### 기억해야 할 내용

- 리스트 컴프리헨션은 lambda 식을 사용하지 않기 때문에 같은 일을 하는 map과 filter 내장함수를 사용한느 것보다 더 명확하다.
- 리스트 컴프리헨션을 사용하면 쉽게 입력 리스트의 원소를 건너뛸 수 있다. 하지만 map을 사용하는 경우에는 filter의 도움을 받아야만 한다.
- 딕셔너리와 집합도 컴프리헨션으로 생성할 수 있다.

## Better Way 28: 컴프리헨션 내부에 제어 하위 식을 세 개 이상 사용하지 말라

컴프리헨션은 기본적인 사용법(Better Way 27 참고) 외에도 루프를 여러 수준으로 내포하도록 허용한다.

컴프리헨션에 하위 식을 두 개 포함시키면 각각의 하위식은 컴프리헨션에 들어간 순서대로 왼쪽에서 오른쪽으로 실행된다.

```python
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in maxtrix for x in row]
print(flat) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
```

다중 루프 사용이 타당한 다른 예로는 2단계 깊이로 구성된 입력 list 구조를 복제하는 경우를 들 수 있다.

예를 들어 이차원 행렬의 원소를 제곱하고 싶다고 하자. 이 컴프리헨션은 []가 더 들어가야 하므로 잡음이 좀 더 많지만 여전히 읽기 쉬운 편이다.

```python
squared = [[x**2 for x in row] for row in matrix]
print(squared) # Output: [[1, 4, 9], [16, 25, 36],[49, 64, 81]]
```

만약 이런 컴프리헨션 안에 다른 루프가 들어 있으면 코드가 너무 길어져서 여러 줄로 나눠 서야 한다.

이 정도가 되면 다중 컴프리헨션이 다른 대안에 비해 더 길어진다.

이런 경우에는 들여쓰기를 사용해 3단계 리스트 컴프리헨션보다 더 명확하게 코드를 작성할 수 있다.

```python
flat = []
for sublist1 in my_lists:
for sublist2 in sublist1:
flat.extend(sublist2)
```

컴프리헨션은 여러 if 조건을 허용한다. 여러 조건을 같은 수준의 루프에 사용하면 암시적으로 and 식을 의미한다. 예를 들어 숫자로 이뤄진 리스트에서 4보다 큰 짝수만 남기고 싶다고 하자. 다음 두 리스트 컴프리헨션이 같은 역할을 한다.

```python
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b = [x for x in a if x > 4 if x % 2 == 0]
c = [x for x in a if x > 4 and x % 2 == 0]
```

각 수준의 for 하위 식의 바로 뒤에 if를 추가함으로써 각 수준마다 조건을 지정할 수 있다.

컴프리 헨션에 들어간느 하위 식이 세 개 이상 되지 않게 제한하라는 규칙을 지켜라.

즉, 조건문 두 개, 루프 두 개, 혹은 조건문 한 개와 루프 한 개를 사용할 수 있다는 뜻이다.

컴프리헨션이 이보다 더 복잡해지면 일반 if와 for문을 사용하고 도우미 함수를 작성하라.

### 기억해야 할 내용

- 컴프리헨션은 여러 수준의 루프를 지원하며 각 수준마다 여러 조건을 지원한다.
- 제어 하위 식이 세 개 이상인 컴프리헨션은 이해하기 매우 어려우므로 가능하면 피해야 한다.

## Better Way 29: 대입식을 사용해 컴프리헨션 안에서 반복 작업을 피하라

컴프리헨션(리스트, 딕셔너리, 집합 중 무엇이든)에서 같은 계산을 여러 위치에서 공유하는 경우가 흔하다.

익셔너리 컴프리헨션을 사용하면 루프의 로직을 더 간결하게 표현할 수 있다.

```python
found = {name: get_batches(stock.get(name, 0}, 8)
for name in order
if get_batches(stock.get(name, 0), 8)}
print(found) # Output: {'나사못': 4, '나비너트': 1}
```

이 코드는 `get_batches(stock.get(name, 0), 8)`이 반복된다는 단점이 있다. 이로 인해 기술적으로는 불필요한 시각적인 잡음이 들어가서 가독성이 나빠진다. 그리고 두 식을 항상 똑같이 변경해야 하므로 실수할 가능성도 높아진다.

이러한 문제에 대한 쉬운 해결 방법은 파이썬 3.8에 도입된 왈러스 연산자(:=)를 사용하는 것이다. 왈러스 연산자를 사용하면 컴프리헨션의 일부분에 대입식을 만들 수 있다.

```python
found = {name: batches for name in order
if (batches := get_batches(stock.get(name, 0), 8))}
```

대입식(`batches := get_batches(…)` )을 사용하면 stock 딕셔너리에서 각 order 키를 한 번만 조회하고 get_batches를 한 번만 호출해서 그 결과를 batches 변수에 저장할 수 있다. 컴프리헨션의 다른 곳에서는 batches 변수를 참조해서 get_batches를 다시 호출할 필요 없이 딕셔너리의 내용을 만들 수 있다. `get_batches` 를 얻기 위한 불필요한 함수 호출을 제거하면 order 리스트 안에 있는 각 요소에 대해 불필요한 연산을 수행하지 않으므로 성능도 향상된다.

대입식을 컴프리헨션의 값 식에 사용해도 문법적으로 올바르다. 하지만 컴프리헨션의 다른 부분에서 이 변수를 읽으려고 하면 컴프리헨션이 평가되는 순서 때문에 실행 시점에 오류가 발생할 것이다.

```python
result = {name: (tenth := count // 10)
for name, count in stock.items() if tenth > 0}
>>>
Traceback ...
NameError: name 'tenth' is not defined
```

대입식을 조건 쪽으로 옮기고 대입식에서 만들어진 변수 이름을 컴프리헨션 값 식에서 참조하면 이 문제를 해결할 수 있다.

```python
result = {name: tenth for name, count in stock.items()
if (tenth := count // 10) > 0}
```

루프 변수는 누출하지 않는 편이 낫다. 따라서 컴프리헨션에서 대입식을 조건에만 사용하는 것을 권장한다.

```python
found = ((name, batches) for name in order
if (batches := get_batches(stock.get(name, 0), 8))
print(next(found))
print(next(found))

>>>
('나사못', 4)
('나비너트', 1)
```

### 기억해야 할 내용

- 대입식을 통해 컴프리헨션이나 제너레이터 식의 조건 부분에서 사용한 값을 같은 컴프리헨션이나 제너레이터의 다른 위치에서 재사용할 수 있다. 이를 통해 가독성과 성능을 향상시킬 수 있다.
- 조건이 아닌 부분에도 대입식을 사용할 수 있지만, 그런 형태의 사용은 피해야 한다.

## Better Way 30: 리스트를 반환하기보다는 제너레이터를 사용하라

시퀀스를 결과로 만들어내는 함수를 만들 때 가장 간단한 선택은 원소들이 모인 리스트를 반환하는 것이다.

### 기억해야 할 내용

- 제너레이터를 사용하면 결과를 리스트에 합쳐서 반환하는 것보다 더 깔끔하다.
- 제너레이터가 반환하는 이터레이터는 제너레이터 함수의 본문에서 yield가 반환한느 값들로 이뤄진 집합을 만들어낸다.
- 제너레이터를 사용하면 작업 메모리에 모든 입력과 출력을 저장할 필요가 없으므로 입력이 아주 커도 출력 시퀀스를 만들 수 있다.

## Better Way 31: 인자에 대해 이터레이션할 때는 방어적이 돼라

### 기억해야 할 내용

- 입력 인자를 여러 번 이터레이션하는 함수나 메서드를 조심하라. 입력받은 인자가 이터레이터면 함수가 이상하게 작동하거나 결과가 없을 수 있다.
- 파이썬의 이터레이터 프로토콜은 컨테이너와 이터레이터가 iter, next 내장 함수나 for 루프 등의 관련 식과 상호작용하는 절차를 정의한다.
- __iter__ 메서드를 제너레이터로 정의하면 쉽게 이터러블 컨테이너 타입을 정의할 수 있다.
- 어떤 값이 (컨테이너가 아닌) 이터레이터인지 감지하려면, 이 값을 iter 내장 함수에 넘겨서 반환되는 값이 원래 값과 같은지 확인하면 된다. 다른 방법으로 collections.abc.Iterator 클래스를 isInstance와 함께 사용할 수도 있다.

## Better Way 32: 긴 리스트 컴프리헨션보다는 제너레이터 식을 사용하라

### 기억해야 할 내용

- 입력이 크면 메모리를 너무 많이 사용하기 때문에 리스트 컴프리헨션은 문제를 일으킬 수 있다.
- 제너레이터 식은 이터레이터처럼 한 번에 원소를 하나씩 출력하기 때문에 메모리 문제를 피할 수 있다.
- 제너레이터 식이 반환한 이터레이터를 다른 제너레이터 식의 하위 식으로 사용함으로써 제너레이터 식을 서로 합성할 수 있다.
- 서로 연결된 제너레이터 식은 매우 빠르게 실행되며 메모리도 효율적으로 사용한다.

## Better Way 33: yield from을 사용해 여러 제너레이터를 합성하라

### 기억해야 할 내용

- yield from 식을 사용하면 여러 내장 제너레이터를 모아서 제너레이터 하나로 합성할 수 있다.
- 직접 내포된 제너레이터를 이터레이션하면서 각 제너레이터의 출력을 보내느 것보다 yield from을 사용하는 것이 성능 면에서 더 좋다.

## Better Way 34: send로 제너레이터에 데이터를 주입하지 말라

### 기억해야 할 내용

- send 메서드를 사용해 데이터를 제너레이터에 주입할 수 있다. 제너레이터는 send로 주입된 값을 yield 식이 반환하는 값을 통해 받으며, 이 값을 변수에 저장해 활용할 수 있다.
- send와 yield from 식을 함께 사용하면 제너레이터의 출력에 None이 불쑥불쑥 나타나는 의외의 결과를 얻을 수도 있다.

## Better Way 35: 제너레이터 안에서 throw로 상태를 변화시키지 말라

### 기억해야 할 내용

- throw 메서드를 사용하면 제너레이터가 마지막으로 실행한 yield 식의 위치에서 예외를 다시 발생시킬 수 있다.
- throw를 사용하면 가독성이 나빠진다. 예외를 잡아내고 다시 발생시키는 데 준비 코드가 필요하며 내포 단계가 깊어지기 때문이다.
- 제너레이터에서 예외적인 동작을 제공하는 더 나은 방법은 __iter__ 메서드를 구현하는 클래스를 사용하면서 예외적인 경우에 상태를 전이시키는 것이다.

## Better Way 36: 이터레이터나 제너레이터를 다룰 때는 itertools를 사용하라

### 기억해야 할 내용

- 이터레이터나 제너레이터를 다루는 itertools 함수는 세 가지 범주로 나눌 수 있다.
- 여러 이터레이터를 연결함
- 이터레이터의 원소를 걸러냄
- 원소의 조합을 만들어냄
- 파이썬 인터프리터에서 help(itertools)를 입력한 후 표시되는 문서를 살펴보면 더 많은 고급 함수와 추가 파라미터를 알 수 있으며, 이를 사용하는 유용한 방법도 확인할 수 있다.

0 comments on commit e729296

Please sign in to comment.