CS 지식/코딩테스트

99클럽 코테 스터디 11일차 TIL | Python 고차함수는 iterator를 반환한다

Julie825 2024. 11. 7. 23:47

함수형 프로그래밍을 공부하다보면 map, filter, reduce, foreach 등을 사용하여 의도가 명확하게 드러나도록 코드를 작성하는 시도를 하게된다.

 

파이썬에도 이러한 접근을 돕기위한 빌트인 라이브러리가 존재한다. 그런데 반환값이 특이하다.

map(변환람다, 원본리스트) # 람다를 적용한 변환 결과를 map 형태로 반환
filter(변환람다, 원본리스트) # 람다 결과가 True인 것만 뽑아서 filter 형태로 반환
zip(키배열, 밸류배열) # 튜플 iterator 반환

 

map, filter를 반환한다는 부분이 이해가 안될 수 있는데, 정말 반환 결과의 타입을 출력해보면 각각 map과 filter로 나온다. 

 

그리고 아래처럼 map은 두번 사용할 수가 없다.

def divide_arr():
    input_map = map(int, input().split(" ")) # map 객체 생성
    
    positive_area = filter(lambda x : x > 0, input_map) # 이 과정에서 input_map이 훼손됨
    print([i for i in input_map]) # 출력 : []
    
    negative_area = map(lambda x : 0 - x, filter(lambda x : x < 0, input_map)) # arr가 없음
    
    print("Positive Area", positive_area) # 출력 : [1, 3]
    print("Negative Area", negative_area) # 출력 : []

 

iterable과 iterator

대체 왜 이런 문제가 생길까? 이를 이해하려면 iterable과 iterator의 차이를 알아야한다.

 

iterable에는 list, set, dict 등이 있다. iter(이터러블)연산의 결과로 iterator를 반환한다.

반면 iterator에는 아까본 map, filter, zip, generator 등이 있다. next(이터레이터)의 결과로 다음 데이터를 읽어올 수 있다.

여러 데이터를 넘겨받기 위한 통로라고 생각하면 된다.

 

우리가 리스트를 for loop로 읽는 과정은 사실상 아래과정을 거쳐온걸로 볼 수 있다.

im_iterable = [1, 2, 3]
temp_iterator = iter(im_iterable)
x1 = next(temp_iterator)
x2 = next(temp_iterator)
x3 = next(temp_iterator)

 

 

아래 예시를 하나 더 보면 차이를 극명하게 구분할 수 있다.

print("ITERABLE")
im_iterable = [1, 2, 3]
for x in im_iterable: # iterable에 iter 연산을 적용하여 새로운 iterator 생성
	print(x) # 출력 1 2 3 
for x in im_iterable: # iterable에 iter 연산을 적용하여 새로운 iterator 생성
	print(x) # 출력 1 2 3 

print("ITERATOR")
im_iterator = map(lambda x : x, im_iterable)
for x in im_iterator: # 주어진 iterator를 읽음
	print(x) # 출력 1 2 3
for x in im_iterator: # 이미 iterator를 끝까지 읽었기에 더 읽을 것이 없음
	print(x) # 출력 x

 

 

list comprehension의 활용

따라서 나처럼 원본을 지켜서 여러번 읽어야하는 경우 iterable을 사용하는 것이 좋다.

단순히 iterator를 리스트로 감싸서 생성할 수도 있고, 아예 list comprehension을 사용할 수도 있다.

 

List comprehension이란 iterable 혹은 iterator로부터 list를 만들어내기 위해 제공되는 문법을 말한다. 아래처럼 사용할 수 있다.

# list comprehension 기본 문법
[변수 for 변수 in 순회가능한_객체]
[변수 for 변수 in 순회가능한_객체 if 필터링조건]

 

사용자 입력을 숫자로 변환하는 map 사용을 아래처럼 대체할 수 있다.

arr = map(int, input().split(" ")) # 기존 map 방식
arr = [int(i) for i in input().split(" ")] # map 대신 list comprehension 사용

 

고차함수 filter를 대체하는 것도 가능하다.

positive_area = [n for n in arr if n > 0]
negative_arean = [i for i in arr if n < 0]

 

zip도 가능하다.

keys, values = ["a", "b", "c"], [1, 2, 3]
zip(keys, values) # zip 연산 결과는 iterator
my_dict = {k : v for k in keys for v in values} # 이 경우 iterable인 dict가 생성됨

 

iterator의 존재이유 

한번밖에 못쓴는 iterator는 대체 왜 필요할까?

iterator는 메모리 절약 측면에서 장점을 가진다.

이들은 필요없어진 이전 연산값을 가비지 컬렉팅될 뿐만 아니라, 값을 사용하기 전 까지는 불러오지 않는 lazy evaluation 전략을 택하기 때문이다.

함수형 프로그래밍의 고질적인 문제로 항상 메모리의 남용이 꼽히는 것을 생각한다면, iterable과 iterator의 차이를 잘 이해하고 꼭 필요한 경우에만 iterable을 사용하는게 좋을 것 같다.