diff --git a/hong/python/effective-python/4. Comprehensions and Generators.md b/hong/python/effective-python/4. Comprehensions and Generators.md new file mode 100644 index 0000000..1696222 --- /dev/null +++ b/hong/python/effective-python/4. Comprehensions and Generators.md @@ -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)를 입력한 후 표시되는 문서를 살펴보면 더 많은 고급 함수와 추가 파라미터를 알 수 있으며, 이를 사용하는 유용한 방법도 확인할 수 있다.