2018년 8월 7일 화요일

Python 의 Filter / Map / Reduce 그리고 Comprehension

파이썬(Python)은 잘 쓰지 않다보니 이 함수형프로그래밍 함수의 대명사 3종의 사용법이 영 익숙해지지 않는다. 그래서 오랜만에 본 블로그 취지(?)로 돌아와서 이 함수들에 대해 간략히 메모한다. 더불어, 이제는 Python 3 로 넘어갈 시점이라 생각해서 모든 정보를 Python 3.7 기준으로 찾아봤다.

글자만 있으면 썰렁해서 넣어보는 이미지 -_-

Filter

Filter 는 일반적으로 리스트의 일부 아이템을 걸러낼 때 사용하는 함수로 파이썬의 것도 동일한 역활이다. 대충 아래와 같이 쓸 수 있다.
>>> input = [1, 2, 3, 4, 5]
>>> filter(lambda x: x % 2 == 0, input)
<filter object at 0x10eaf4550>
Python 2.7.x 의 경우 위 filter 는 바로 리스트를 리턴하지만 Python 3에서는 이터레이터를 리턴하기 때문에 REPL에서 결과를 바로 확인 할 수 없다. 물론 그렇다고 고민할 필요는 없다. 리스트로 바꾸면 바로 확인 가능하다.
>>> list(filter(lambda x: x % 2 == 0, input))
[2, 4]
아예 list 를 쓰는 방식으로 익숙해지면 좋다. 이제는 Python 3 로 가야 할 시대다.

lambda 는 Swift 의 클로져와 개념적으로 비슷한 단순함수다. 당연히 일반 함수를 넣어도 잘 동작한다.
>>> def is_even(value):
...     return value % 2 == 0
...
>>> list(filter(is_even, input))
[2, 4]
식이 복잡하다면 굳이 람다를 쓰지 말고 함수로 구현하면 된다는 이야기다.

리스트가 아닌 사전형(dictionary)을 입력으로 넣을 수도 있다. 이 경우 함수의 입력값이 키(key)로 바뀐다는 점에 주의하자.
>>> indict = {'a': 100, 'b': 200}
>>> list(filter(lambda x: indict[x] > 100, indict))
['b']
심지어 결과도 키의 리스트가 되어버린다. 좀 어이없는 것 같다.

그런데, Python 에서는 이 filter 함수가 잘 안쓰인다. 대신 아래와 같이 for 루프를 이용하는 코드를 쓰는 것이 좀 더 일반적(?)이다.
>>> [v for v in input if v % 2 == 0]
[2, 4]
이런 스타일을 Comprehension 이라고 부른다. 리스트를 생성하므로 이 경우는 List Comprehension 이라고 한다. 한국어로는 번역을 뭐라고 해야할지 모르겠다.

왜 filter 함수가 잘 안쓰일까? 이유를 확실히 찾아보진 않았지만 개인적인 기준으론 for 문을 이용하는 스타일이 더 읽기 쉽다. 이게 핵심이지 않을까?

이 for 루프스타일을 잘 활용하면 앞서 본 사전형 필터 예제도 사전형을 리턴하게 만들 수 있다.
>>> {k: v for k, v in indict.items() if v > 100}
{'b': 200}
참고로 items() 함수는 사전형의 각 아이템을 (key, value) 튜플 리스트로 변경해서 넘겨준다. Python 2.7 에서는 items() 가 아닌 iteritems() 함수를 대체로 사용 할 수 있다. iteritems() 는 이터레이터를 리턴하지만 사용법은 똑같다.

참고로 이 코드의 경우 사전형을 리턴하므로 Dictionary Comprehension 이라고 할 수 있다.

하여간에 아무리봐도 Python 에서는 filter 대신 이 방법(Comprehension)을 쓰는게 더 간단하고 읽기도 편해보이고 하여간 좋아보인다. 굳이 이터레이터가 필요한게 아니라면 filter 함수는 잊어도 될 것 같다.

Map

map 은 콜렉션을 순회하면서 다른 리스트를 만들어내는 기능이다. 말로 설명하면 좀 이해가 안되니 아래 예제를 보자. 리스트의 값을 모두 2배로 바꾸는 예제다
>>> list(map(lambda x: x * 2, input))
[2, 4, 6, 8, 10]
Python 3.7 의 map 함수도 이터레이터를 리턴한다. 그래서 REPL에서 바로는 확인이 안되니 리스트로 바꿔서 값을 확인한 예제다. 물론 Python 2.7 등에서는 굳이 list 로 바꾸지 않아도 된다. 만약 호환성 있는 코드를 쓰고 싶다면 list 를 습관적으로 붙이는게 당연히 좋을 것이다.

map 함수도 for 루프 스타일(List Comprehension)로 만들 수 있다.
>>> [v * 2 for v in input]
[2, 4, 6, 8, 10]
앞서 본 filter 예제에서 if 절이 빠진 느낌으로 보이면 정답이다. 읽기 편하다. 아무리봐도 역시 이것이 파이썬 스타일 같다고 느껴진다.

사전형도 굳이 map 을 쓰기보다는 루프스타일(Dictionary Comprehension)로 써 보자.
>>> {k: v * 2 for k, v in indict.items()}
{'a': 200, 'b': 400}
파이썬틱하게 아름답다. 참고로 앞서 filter 예제에서 이야기 했다시피 items() 메소드는 Python 3 용이고 Python 2.7 에서는 iteritems() 를 사용하면 된다.

Reduce

reduce 는 생각보다는 자주 쓰이지 않는 녀석이지만 왠지 filter 와 map 이 나오면 따라붙는 녀석 같다. 각 리스트의 값을 이용해 하나의 값을 만들어내는 함수 - 예를 들어 리스트 아이템을 모두 더하는 등 - 인데 이 외의 용도를 잘 알기가 힘들 정도로 잘 안쓰인다. Swift 에서도 제대로 활용하기 힘들 정도로 까다로운 녀석이었는데 파이썬은 어떨까?
>>> reduce(lambda x, y: x + y, input)
Traceback (most recent call last):
  File "<stdin>", line 1, in 
NameError: name 'reduce' is not defined
아 이런 기쁘게도(?) 없다.

잠깐 찾아보니 Python 3 부터 reduce가 builtin 에서 사라졌다고 한다. 만약 Python 3 에서 reduce 를 쓰고 싶다면 별도의 패키지에서 import 해야한다.
>>> from functools import reduce
>>> reduce(lambda x, y: x + y, input)
15
Python 2.7.x 에서는 여전히 reduce 는 별도의 import 필요 없이 그냥 쓸 수 있었다.

솔직히 말해서 reduce 는 아직도 참 난해한 코드라고 생각된다. 저 코드를 딱 봤을 때 바로 연산 내용이 머리속에서 연상된다면 모르겠지만 개인적으로는 바로 연상이 되지 않는다. 특히 람다함수로 넘어가는 두 개의 인자 배치를 생각해보자. 현재는 리스트가 5개의 아이템이라 크게 어렵진 않은데 만약 매우 많아진다면 상상하기 귀찮을지도 모른다. (혹시 나만 그런가? 🤔)

파이썬의 창시자 귀도 반 로썸(Guido van Rossum)의 말 하나를 인용해보자.
"So now reduce(). This is actually the one I've always hated most, because, apart from a few examples involving + or *, almost every time I see a reduce() call with a non-trivial function argument, I need to grab pen and paper to diagram what's actually being fed into that function before I understand what the reduce() is supposed to do. So in my mind, the applicability of reduce() is pretty much limited to associative operators, and in all other cases it's better to write out the accumulation loop explicitly."
다행히도 그도 reduce 를 존나 싫어한다.

마무리

Swift 에도 이런 Comprehension 스타일의 코드가 도입되면 편하려나 라는 생각을 잠깐 해 봤는데, Swift 의 map 이나 filter 는 꼬리클로져를 이용하면 Swift 다운 간략하고 읽기 편한 코드를 짤 수 있으니  굳이 필요는 없겠다는 생각이 들었다.

Python 에는 Python 에 어울리는 스타일이 역시 좋은 것 같다. 이건 모든 언어에도 통용되는 법칙이다. 각 언어에는 그 언어에 어울리는 스타일이 있는 법이다. 굳이 남의 것을 따라할 이유는... 있을수도 있겠지만 꼭 필요한 것도 아니니까 😎

[관련글] Swift - Collection 타입의 도구들: map, filter, reduce, zip

댓글 없음 :