Chapter 4. 컴프리헨션과 제너레이터를 읽으면서 정리합니다.
새로 알게 되었거나 중요하다고 생각되는 부분 위주로 정리하고자 합니다.
Effective Python 2nd
Chapter 4. 컴프리헨션과 제너레이터
27. map과 filter 대신 컴프리헨션을 사용하라
-
리스트 컴프리헨션은 lambda 식을 사용하지 않기 때문에 같은 일을 하는 map과 filter 내장 함수를 사용하는 것보다 명확하다
squares = [x**2 for x in 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} threes_cubed_set = {x**3 for x in a if x % 3 == 0}
28. 컴프리헨션 내부에 제어 하위 식을 세 개 이상 사용하지 말라
-
제어 하위 식이 세 개 이상인 컴프리헨션은 이해하기 매우 어려우므로 가능하면 피하자.
# 아래보다는 flat = [x for sublist1 in my_lists for sublist2 in sublist1 for x in sublist2] # 아래가 더 읽기 편하다. flat = [] for sublist1 in my_lists: for sublist2 in sublist1: flat.extend(sublist2)
29. 대입식을 사용해 컴프리헨션 안에서 반복 작업을 피하라
-
대입식을 통해 조건 부분에서 사용한 값을 같은 컴프리헨션이나 제너레이터의 다른 위치에서 재사용할 수 있다.
이를 통해 가독성과 성능을 향상시킬 수 있다.
found = {name: batches for name in order if (batches := get_batches(stock.get(name, 0), 8))}
-
조건이 아닌 부분에도 대입식을 사용할 수 있지만, 그런 형태의 사용은 피하자
# for 문안의 count가 누출됨 half = [(last := count // 2) for count in stock.values()] # 아래의 예시처럼 조건식에만 사용하자 order = ['나사못', '나비너트', '클립'] found = ((name, batches) for name in order if (batches := get_batches(stock.get(name, 0), 8))) print(next(found)) print(next(found))
30. 리스트를 반환하기 보다는 제너레이터를 사용하라
-
제너레이터를 사용하면 결과를 리스트에 합쳐서 반환하는 것보다 깔끔하다.
# 결과를 리스트에 합쳐서 반환 def index_words(text): result = [] if text: result.append(0) for index, letter in enumerate(text): if letter == ' ': result.append(index + 1) return result # 제너레이터 사용 def index_words_iter(text): if text: yield 0 for index, letter in enumerate(text): if letter == ' ': yield index + 1
-
제너레이터를 사용하면 작업 메모리에 모든 입력과 출력을 저장할 필요가 없으므로 입력이 아주 커도 출력 시퀀스를 만들 수 있다.
def index_file(handle): offset = 0 for line in handle: if line: yield offset for letter in line: offset += 1 if letter == ' ': yield offset import itertools with open('address.txt', 'r', encoding='utf-8') as f: it = index_file(f) results = itertools.islice(it, 0, 10) print(list(results))
31. 인자에 대해 이터레이션할 때는 방어적이 돼라
-
입력 인자를 여러 번 이터레이션 함수나 메서드를 조심하라.
입력받은 인자가 이터레이터면 함수가 이상하게 작동하거나 결과가 없을 수 있다.
def read_visits(data_path): with open(data_path) as f: for line in f: yield int(line) it = read_visits('my_numbers.txt') percentages = normalize(it) print(percentages) # 결과 # []
-
파이썬 이터레이터 프로토콜은 컨테이너와 이터레이터가 iter, next 내장 함수나 for 루프 등의 관련 식과 상호작용하는 절차를 정의한다.
class ReadVisits: def __init__(self, data_path): self.data_path = data_path def __iter__(self): with open(self.data_path) as f: for line in f: yield int(line)
-
어떤 값이 이터레이터인지 감지하려면 이 값을 iter 내장 함수에 넘겨서 반환되는 값이 확인하면 된다.
다른 방법으로 collections.Iterator 클래스를 isinstance와 함께 사용할수 있다.
def normalize_defensive(numbers): if iter(numbers) is numbers: # 이터레이터 -- 나쁨! raise TypeError('컨테이너를 제공해야 합니다') total = sum(numbers) result = [] for value in numbers: percent = 100 * value / total result.append(percent) return result # 아래의 방법을 추천한다. from collections.abc import Iterator def normalize_defensive(numbers): # 반복 가능한 이터레이터인지 검사하는 다른 방법 if isinstance(numbers, Iterator): raise TypeError('컨테이너를 제공해야 합니다') total = sum(numbers) result = [] for value in numbers: percent = 100 * value / total result.append(percent) return result visits = [15, 35, 80] it = iter(visits) normalize_defensive(it) # TypeError: 컨테이너를 제공해야 합니다
32. 긴 리스트 컴프리헨션보다는 제너레이터 식을 사용하라
-
입력이 크면 메모리를 너무 많이 사용하기 때문에 리스트 컴프리헨션은 문제를 일으킬 수 있다.
-
제너레이터 식은 이터레이처럼 한 번에 원소를 하나씩 출력하기 때문에 메모리 문제를 피할 수 있다.
value = [len(x) for x in open('my_file.txt')] print(value) it = (len(x) for x in open('my_file.txt')) print(next(it)) print(next(it))
-
제너레이터 식이 반환한 이터레이터를 다른 제너레이터 식의 하위 식으로 사용함으로써 제너레이터 식을 서로 합성할 수 있다.
roots = ((x, x**0.5) for x in it)
33. yield from을 사용해 여러 제너레이터를 합성하라
-
yield from 식을 사용하면 여러 내장 제너레이터를 모아서 제너레이터 하나로 합성할 수 있다.
def move(period, speed): for _ in range(period): yield speed def pause(delay): for _ in range(delay): yield 0 def animate_composed(): yield from move(4, 5.0) yield from pause(3) yield from move(2, 3.0)
-
직접 내포된 제너레이터를 이터레이션하면서 각 제너레이터를 출력을 내보내는 것보다 yield from을 사용하는 것이 성능 면에서 더 좋다.
import timeit def child(): for i in range(1_000_000): yield i def slow(): for i in child(): yield i def fast(): yield from child() baseline = timeit.timeit( stmt='for _ in slow(): pass', globals=globals(), number=50) print(f'수동 내포: {baseline:.2f}s') comparison = timeit.timeit( stmt='for _ in fast(): pass', globals=globals(), number=50) print(f'합성 사용: {comparison:.2f}s') reduction = -(comparison - baseline) / baseline print(f'{reduction:.1%} 시간이 적게 듦') # 수동 내포: 4.01s # 합성 사용: 3.49s # 13.0% 시간이 적게 듦
34. send로 제너레이터에 데이터를 주입하지 말라
-
합성할 제너레이터들의 입력으로 이터레이터를 전달하는 방식이 send를 사용하는 방식보다 더 낫다.
send는 가급적 사용하지 말라.
def wave_cascading(amplitude_it, steps): step_size = 2 * math.pi / steps for step in range(steps): radians = step * step_size fraction = math.sin(radians) amplitude = next(amplitude_it) # 다음 입력 받기 output = amplitude * fraction yield output def complex_wave_cascading(amplitude_it): yield from wave_cascading(amplitude_it, 3) yield from wave_cascading(amplitude_it, 4) yield from wave_cascading(amplitude_it, 5) def run_cascading(): amplitudes = [7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10] it = complex_wave_cascading(iter(amplitudes)) for amplitude in amplitudes: output = next(it) transmit(output) run_cascading()
35. 제너레이터 안에서 throw 상태로 변화시키지 말라
-
throw 메서드를 사용하면 제너레이터가 마지막으로 실행한 yield 식의 위치에서 예외를 다시 발생시킬 수 있다.
그러나 가독성이 나빠진다.
def run(): it = timer(4) while True: try: if check_for_reset(): current = it.throw(Reset()) else: current = next(it) except StopIteration: break else: announce(current)
-
더 나은 방법으로
__iter__
메서드를 구현하는 클래스를 사용하면서 예외적인 경우에 상태를 전이시키는 것이다.class Timer: def __init__(self, period): self.current = period self.period = period def reset(self): self.current = self.period def __iter__(self): while self.current: self.current -= 1 yield self.current def run(): timer = Timer(4) for current in timer: if check_for_reset(): timer.reset() announce(current) run()
36. 이터레이터나 제너레이터를 다룰 때는 itertools를 사용하라
-
여러 데이터 연결하기
-
chain
import itertools it = itertools.chain([1, 2, 3], [4, 5, 6]) # [1, 2, 3, 4, 5, 6]
-
repeat
it = itertools.repeat('안녕', 3) # ['안녕', '안녕', '안녕']
-
cycle
it = itertools.cycle([1, 2]) result = [next(it) for _ in range (10)] # [1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
-
tee
한 이터레이터를 병렬적으로 두 번째 인자로 지정된 개수의 이터레이터를 만들 때 사용한다.
이 함수로 만들어진 각 이터레이터를 소비하는 속도가 같지 않으면, 처리가 덜 된 이터레이터의 원소를 큐에 담아둬야 하므로 메모리 사용랑이 늘어난다.
it1, it2, it3 = itertools.tee(['하나', '둘'], 3) # ['하나', '둘'] # ['하나', '둘'] # ['하나', '둘']
-
zip_longest
keys = ['하나', '둘', '셋'] values = [1, 2] normal = list(zip(keys, values)) # zip: [('하나', 1), ('둘', 2)] it = itertools.zip_longest(keys, values, fillvalue='없음') longest = list(it) #zip_longest: [('하나', 1), ('둘', 2), ('셋', '없음')]
-
-
이터레이터에서 원소 거르기
-
islice
끝만 지정, 시작과 끝 지정
시작, 끝, 증가값 지정
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] first_five = itertools.islice(values, 5) print('앞에서 다섯 개:', list(first_five)) # 앞에서 다섯 개: [1, 2, 3, 4, 5] middle_odds = itertools.islice(values, 2, 8, 2) print('중간의 홀수들:', list(middle_odds)) # 중간의 홀수들: [3, 5, 7]
-
takewhile
주어진 술어가 False를 반환하는 첫 원소가 나타날 때까지 원소를 돌려준다.
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] less_than_seven = lambda x: x < 7 it = itertools.takewhile(less_than_seven, values) print(list(it)) # [1, 2, 3, 4, 5, 6]
-
dropwhile
takewhile의 반대
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] less_than_seven = lambda x: x < 7 it = itertools.dropwhile(less_than_seven, values) print(list(it)) # [7, 8, 9, 10]
-
filterfalse
filter 내장 함수의 반대
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] evens = lambda x: x % 2 == 0 filter_result = filter(evens, values) print('Filter:', list(filter_result)) # Filter: [2, 4, 6, 8, 10] filter_false_result = itertools.filterfalse(evens, values) print('Filter false:', list(filter_false_result)) # Filter false: [1, 3, 5, 7, 9]
-
-
이터레이터에서 원소의 조합 만들어내기
-
accumulate
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] sum_reduce = itertools.accumulate(values) print('합계:', list(sum_reduce)) # 합계: [1, 3, 6, 10, 15, 21, 28, 36, 45, 55] def sum_modulo_20(first, second): output = first + second return output % 20 modulo_reduce = itertools.accumulate(values, sum_modulo_20) print('20으로 나눈 나머지의 합계:', list(modulo_reduce)) # 20으로 나눈 나머지의 합계: [1, 3, 6, 10, 15, 1, 8, 16, 5, 15]
-
product
데카르트의 곱 반환
single = itertools.product([1, 2], repeat=2) print('리스트 한 개:', list(single)) # 리스트 한 개: [(1, 1), (1, 2), (2, 1), (2, 2)] multiple = itertools.product([1, 2], ['a', 'b']) print('리스트 두 개:', list(multiple)) # 리스트 두 개: [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
-
permutations (순열)
it = itertools.permutations([1, 2, 3, 4], 2) print(list(it)) # [(1, 2), (1, 3), (1, 4), (2, 1), (2, 3), (2, 4), (3, 1), (3, 2), (3, 4), (4, 1), (4, 2), (4, 3)]
-
combinations (조합)
it = itertools.combinations([1, 2, 3, 4], 2) print(list(it)) # [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
-
combinations_with_replacement (중복조합)
it = itertools.combinations_with_replacement([1, 2, 3, 4], 2) print(list(it)) [(1, 1), (1, 2), (1, 3), (1, 4), (2, 2), (2, 3), (2, 4), (3, 3), (3, 4), (4, 4)]
-