[Effective Python] Comprehensions and Generators

u8cnk1·2023년 1월 10일
0

Chapter 4: Comprehensions and Generators

Item 27: map 대신 Comprehension 사용하기

파이썬은 다른 시퀀스 또는 반복 가능한 객체에서 새 목록을 파생하기 위한 list comprehensions 구문을 제공한다.

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares = []

# 예) 리스트에서 각 숫자의 제곱 계산

# for문 이용
for x in a:				   
	squares.append(x**2)
print(squares)

# List comprehension
squares = [x**2 for x in a] 
print(squares)

# map
alt = map(lambda x: x ** 2, a)
print(list(alt))

list comprehension은 람다 식을 필요로 하지 않기 때문에 map을 사용하는 것보다 명확하게 이해할 수 있다. 또한 list comprehension에 조건식을 추가함으로써 list에서 특정 항목을 쉽게 필터링 할 수 있다. 이 동작은 filter 없이는 map에서 지원되지 않는다.

# 예) 2의 배수인 item만 제곱하기
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# list comprehension
even_squares = [x**2 for x in a if x % 2 == 0]

# map의 filter 기능 이용
alt = map(lambda x: x**2, filter(lambda x: x % 2 == 0, a))

위 코드에서 map의 filter 기능을 이용한 list(alt)가 list comprehension을 이용한 even_squares와 동일한 결과를 나타내지만 훨씬 읽기 어려운 것을 알 수 있다.


dictionary와 set 또한 comprehension을 사용하여 생성될 수 있다.

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# dictionary 생성
even_squares_dict = {x: x**2 for x in a if x % 2 == 0} # list comprehension
alt_dict = dict(map(lambda x: (x, x**2),			   # map
				filter(lambda x: x % 2 == 0, a)))


# set 생성
threes_cubed_set = {x**3 for x in a if x % 3 == 0}		#list comprehension
alt_set = set(map(lambda x: x**3,
			 filter(lambda x: x % 3 == 0, a)))			# map

마찬가지로 map과 filter를 사용하여 list comprehension과 동일한 결과를 얻을 수 있다. 하지만 이 방법을 사용하면 문장이 길어져서 여러 줄에 걸쳐 분할해야 하고, 이는 훨씬 더 많은 노이즈를 유발한다.

Item 28: Comprehensions에서 여러 하위 표현식 사용하지 않기

comprehension은 여러개의 하위 표현식을 포함하여 loop의 각 레벨에서 여러 조건을 지원할 수 있으며, 하위 표현식은 제공된 순서대로 왼쪽에서 오른쪽으로 실행된다. 하지만 하위 표현식이 많아지면 이해하기 어렵고 혼동할 가능성이 커지기 때문에 하위 표현식이 2개보다 많은 경우 if나 helper function을 사용하는 것이 좋다.

Item 29: 할당 표현식을 사용하여 Comprehension에서 반복 방지

할당 표현식은 comprehension과 생성자(generator) 표현식이 동일한 comprehension의 다른 곳에서 한 조건의 값을 재사용하는 것을 가능하게 하여 가독성과 성능을 향상시킬 수 있다.
파이썬 3.8에 도입된 :=연산지를 사용하여 comprehension의 일부로 할당 표현식을 형성할 수 있다. 이때 할당식을 조건(if문 등)으로 이동한 다음 comprehension의 값 식을 정의한 변수 이름을 참조하는 것이 권장된다.

# 예) 고객의 주문을 이행할 수 있는 충분한 재고(8개 묶음)가 있는지 확인하기
stock = {
	'nails': 125,
 	'screws': 35,
 	'wingnuts': 8,
 	'washers': 24,
	}
order = ['screws', 'wingnuts', 'clips']
def get_batches(count, size):
	 return count // size

# 1
found = {name: get_batches(stock.get(name, 0), 8)
 		for name in order
 		if get_batches(stock.get(name, 0), 8)}
print(found)			# {'screws': 4, 'wingnuts': 1}

# 2
found = ((name, batches) for name in order
 		if (batches := get_batches(stock.get(name, 0), 8)))
print(next(found))		# ('screws', 4)
print(next(found))		# ('wingnuts', 1)

#2와 같이 :=연산자를 사용하여 get_batches가 중복 호출되는 것을 방지할 수 있다.

Item 30: List 리턴하는 대신 Generator 고려하기

generator를 사용하면 함수가 결과가 누적된 list를 반환하는 것보다 더 명확할 수 있다.

address = 'Four score and seven years ago...'

#1: append 기능을 사용하여 결과를 list에 축적하고 리턴
def index_words(text):
 	result = []
 	if text:
 		result.append(0)
 	for index, letter in enumerate(text):
 		if letter == ' ':
 			result.append(index + 1)
 	return result
 
result = index_words(address)
print(result[:10])

generator는 yield 표현식을 사용하는 함수에 의해 생성된다.

#2: generator 사용하여 동일한 결과를 생성하는 함수 정의
def index_words_iter(text):
	if text:
 		yield 0
 	for index, letter in enumerate(text):
 		if letter == ' ':
 	yield index + 1
 
 
 it = index_words_iter(address)
print(next(it))
print(next(it))

generator에서 반환된 iterator는 함수의 본문 내에서 식을 산출하기 위해 전달된 값 집합을 생성한다.
generator는 작업 메모리가 모든 입력과 출력을 포함하지 않기 때문에 임의로 큰 입력에 대한 일련의 출력을 생성할 수 있다.

Item 31: Arguments 반복할 때 주의하기

함수가 list를 매개변수로 사용하는 경우 해당 list를 여러 번 반복하는 것이 중요하다. 그러나 StopItation 예외가 이미 발생한(이미 소진된) generator 또는 iterator를 통해 반복하는 경우 더이상 결과를 얻을 수 없다. 이 문제를 해결하기 위해 input iterator를 명시적으로 모두 사용하고 전체 내용의 복사본을 list에 보관하게 되면 프로그램의 메모리가 부족해진다. 대안으로 호출될 때마다 새 iterator를 반환하는 함수를 허용하면 함수를 사용하기 위해 generator를 호출하고 매번 새 iterator를 생성하는 람다 식을 전달할 수 있다. 저자는 동일한 결과를 얻는 더 좋은 방법으로 iterator protocol 을 구현하는 새로운 container class를 제공하는 것을 제안한다. iterator protocal은 container와 iterator가 iter와 next 내장함수, for loops등에서 상호 작용하는 방식을 정의한다.
__iter__ 메서드를 ganerator로 구현하여 사용자 고유의 컨테이너 유형을 쉽게 정의할 수 있으며, 또는 isinstance 기본 제공 함수를 collections.abc.Iterator 클래스와 함께 사용할 수 있다.

Item 32: Generator Expressions (for Large List Comprehensions)

list comprehension은 입력 시퀀스의 각 값에 대해 하나의 항목을 포함하는 새 list 인스턴스를 만들 수 있지만, 큰 입력의 경우 이 동작은 상당한 양의 메모리를 소모하고 프로그램이 중단될 수 있다. 파이썬은 이 문제를 해결하기 위해 generator expressions 을 제공한다. () 문자 사이에 목록 이해와 같은 구문을 넣어 생성자 식을 만든다.(예: it = (len(x) for x in open('my_file.txt')))
generator expressions는 한 generator expressions에서 generator expressions의 하위 식으로 반복기를 전달하여 구성할 수 있고, iterator로 출력을 한 번에 하나씩 생성하여 메모리 문제를 방지한다. 이와 같이 generator를 함께 연결하는 것은 파이썬에서 매우 빠르게 실행된다. 대규모 입력 스트림에서 작동하는 기능을 구성하는 방법을 찾고 있을 때 generator expressions을 사용하는 것이 좋다. 단, 이러한 iterator를 두 번 이상 사용하지 않도록 주의해야 한다.

Item 33: yield from 사용하여 Multiple Generators 구성

yield from을 사용하면 여러 중첩 generator를 하나의 결합된 generator로 함께 구성할 수 있다. 이는 파이썬 인터프리터가 loop 및 yield 표현식의 중첩을 처리하여 중첩된 generator를 수동으로 반복하는 것보다 성능이 향상되도록 한다.

Item 34: Generators에 데이터를 주입할 때 send 사용하지 않기

yield expression은 연속 출력 값을 생성하는 간단한 방법으로 generator 기능을 제공한다. 그러나 이 채널은 단방향으로 나타나고, generator가 실행되는 동안 데이터를 동시에 스트리밍할 수 없다. 파이썬 generator는 yield expression을 양방향 채널로 업그레이드 하는 send 메소드를 지원한다.(send method를 사용하여 yield expression에 변수에 할당할 수 있는 값을 제공하여 generator에 데이터를 주입할 수 있다.) send method는 출력을 산출하는 동시에 generator에 스트리밍 입력을 제공하는 데 사용될 수 있고 일반적으로 generator를 반복할 때 yield expression의 값은 None 이다. for loop 또는 next 함수로 generator를 반복하는 대신 send 메소드를 호출하면 제공된 매개변수가 generator가 재개될 때 yield expression의 value값이 된다.(generator가 처음 시작할 때는 yield expression을 아직 발견하지 못했기 때문에 초기 send 호출의 값은 None)
yield from을 사용하면 여러 generator를 함께 구성할 수 있지만 send와 yield from을 같이 사용하면 예기치 않은 시간에 generator에 None이 출력되는 등의 문제가 발생할 수 있다. 저자는 send method 보다 iterator를 사용하는 것을 권하고 있다.

Item 35: Avoid Causing State Transitions in Generators with 'throw'

yield from과 send method 외에 generator 함수 내에서 Exception 인스턴스를 발생시키는throw method가 있다. throw method가 호출되면 다음에 발생하는 yield expression은 정상적으로 출력되는 대신 제공된 Expection 인스턴스를 다시 발생시킨다. 그러나 예외를 탐지하거나 throw, 다음 호출을 결정하는 데 다양한 level에서 코드 중첩이 생기고 노이즈가 발생한다. 반복가능한 container 객체를 사용하여 이 기능을 구현하면 중첩이 감소되어 훨씬 더 간단한 반복을 수행할 수 있다. 저자는 예외적인 행동이 필요할 때 throw 사용을 피하고 __iter__ 메소드를 구현하는 클래스를 사용할 것을 제안하고 있다.

Item 36: itertools

itertools module은 iterator를 구성하고 상호작용하는데 유용한 많은 기능을 포함한다. -> help(itertools) 참고

Linking Iterators

: iterator를 연결하기 위한 여러 기능을 포함

  1. chain
    • 여러 iterator들을 하나의 sequential iterator로 결합할 수 있다.
it = itertools.chain([1, 2, 3], [4, 5, 6])
print(list(it))			# [1, 2, 3, 4, 5, 6]
  1. repeat
    • 단일 값을 무한히 출력하거나, 두 번째 매개변수를 사용하여 최대 횟수를 지정한다.
it = itertools.repeat('hello', 3)
print(list(it))			# ['hello', 'hello', 'hello']
  1. cycle
    • iterator의 항목을 반복한다.
it = itertools.cycle([1, 2])
result = [next(it) for _ in range (10)]
print(result)			# [1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
  1. tee
    • single iterator를 두 번째 매개변수로 지정된 parallel iterator의 수만큼 분할한다.
it1, it2, it3 = itertools.tee(['first', 'second'], 3)
print(list(it1))			# ['first', 'second']
print(list(it2))			# ['first', 'second']
print(list(it3))			# ['first', 'second']
  1. zip_longest
    • 변형된 zip 함수
    • iterator가 소진될 때 자리 표시자 값을 리턴, iterator의 길이가 다를 경우 발생할 수 있다.
keys = ['one', 'two', 'three']
values = [1, 2]

#zip
normal = list(zip(keys, values))
print('zip: ', normal)					#  [('one', 1), ('two', 2)]

#zip_longest
it = itertools.zip_longest(keys, values, fillvalue='nope')
longest = list(it)
print('zip_longest:', longest)			# [('one', 1), ('two', 2), ('three', 'nope')]

Filtering Items

: iterator에서 item을 필터링하기 위한 여러 기능을 포함

  1. islice
    • islice를 사용하여 복사하지 않고 숫자 인덱스를 기준으로 iterator를 슬라이스
    • end / start, end / start, end, step을 지정할 수 있다.
    • sequence slicing 및 striding과 유사
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

first_five = itertools.islice(values, 5)
print('First five: ', list(first_five))			# [1, 2, 3, 4, 5]

middle_odds = itertools.islice(values, 2, 8, 2)
print('Middle odds:', list(middle_odds))		# [3,5,7]
  1. takewhile
    • 함수가 False를 반환할때까지 iterator에서 item을 반환
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]
  1. dropwhile
    • takewhile 의 반대
    • 함수가 처음으로 True를 반환할 때까지 항목을 건너뛴다.
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]
  1. filterfalse
    • filter 함수의 반대
    • 함수가 False를 반환하는 iterator에서 모든 항목을 반환
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))			# [2, 4, 6, 8, 10]

filter_false_result = itertools.filterfalse(evens, values)
print('Filter false:', list(filter_false_result))	# [1, 3, 5, 7, 9]

Producing Combinations of items from iterators

: iterator에서 item을 조합을 생성하기 위한 여러 기능을 포함

  1. accumulate
    • 두 매개변수를 사용하는 함수를 적용하여 각 입력 값에 대해 누적된 결과를 출력
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
sum_reduce = itertools.accumulate(values)
print('Sum: ', 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('Modulo:', list(modulo_reduce))			# [1, 3, 6, 10, 15, 1, 8, 16, 5, 15]
  1. product
    • 하나 이상의 iterator로부터 항목의 Cartesian 곱을 반환
      이는 깊이 중첩된 목록 이해를 사용하는 것에 대한 좋은 대안이다
single = itertools.product([1, 2], repeat=2)
print('Single: ', list(single))			# [(1, 1), (1, 2), (2, 1), (2, 2)]

multiple = itertools.product([1, 2], ['a', 'b'])
print('Multiple:', list(multiple))		# [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
  1. permutations
    • iterator의 항목과 함께 길이가 N의 고유한 순서 순열을 반환
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)]
  1. combinations
    • 길이 N과 반복되지 않은 항목의 순서 없는 조합을 반환
it = itertools.combinations([1, 2, 3, 4], 2)
print(list(it))			# [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
  1. combinations_with_replacement
    • combinations와 동일하지만 반복되는 값을 허용
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)]

0개의 댓글