티스토리 뷰

반응형

맵과 필터는 리스트의 원소에 특정한 함수를 적용하여, 리스트의 내용을 바꾸는 조작이다. 실제로는 리스트 축약(List Comprehension) 문법으로 대체하여 더 많이 사용되기 때문에 함수의 사용법 자체는 사실 크게 중요하지 않을 수 있다. 하지만 리스트를 맵과 필터 (그리고 리듀스)를 이용해서 조작하고 다루는 방식은 이른바 함수형 패러다임에서 가장 기본적이고 중요한 멘탈 모델이기 때문에 익혀둘 필요가 있다. 물론 파이썬이 함수형 언어는 아니지만, 이 개념에 익숙해지면 코드를 더욱 간결하고 쉽게 짤 수 있는 기본기를 갖게 된다.

사상(mapping)

맵과 필터에서 가장 기본이 되는 개념은 바로 맵핑 그 자체이다. 함수를 입력을 받아 데이터를 가공하고 출력하는 장치로서 이해하는 개념이 일반적인데1, 이 "적용"의 방향을 거꾸로 보는 시각 역시 유용하다. 즉 입출력이 아니라 그 내부에서 값을 조작하는 변환(transformation)으로 함수를 보는 것이다.

예를 들어 어떤 주어진 값에 1을 더하는 함수 f가 있다고 가정하자. 그리고 3이라는 값이 있다. 여기서 '적용'이라는 개념은 다음 두 가지 상황으로 이해할 수 있다.

  1. 장치 f 에 3이라는 값을 넣어 4라는 결과를 획득한다.
  2. 3이라는 값에 변형 f 를 적용하여 4로 만들었다.

이 때의 두 번째 상황이 바로 맵핑이다. 즉 3이라는 값에 함수 f를 사상(mapping)하여 4로 변환할 수 있는 것이다.

이와 같은 이해가 중요한 것은 파이썬 코딩 그 자체 보다는 프로그래밍 언어에서의 함수를 수학에서의 함수와 같은 개념으로 이해할 수 있다는 점이다.2 우리가 수학시간에 배우는 함수는 정의역과 변역을 1:1 혹은 n:1로 대응시키는 것으로 설명되는데, 이는 결국 정의역이라는 유한 혹은 무한한 값의 집합의 각 원소에 대해서 함수를 사상하여 변역으로 바꿀 수 있다는 개념이다.

맵과 필터는 리스트 뿐만 아니라 집합(set), 튜플, 반복자, 생성자등의 컨테이너 타입에 대해서 그 내부 원소에 어떠한 함수를 사상하여 결과를 만들어 내는 리스트를 조작하는 방법이며, 기본 함수 map(), filter()로 정의되어 있다.

map(함수, 반복가능한타입) --> map 객체
filter(함수, 반복가능한타입) --> filter 객체

여기서 결과가 map, filter 객체라는 점에 대해서는 크게 신경쓰지 않아도 된다. 이들은 각각 map, filter 함수의 결과로 생성된 결과물이며, range() 함수와 같이 일종의 느긋한 반복자라고 보면 된다. 리스트가 아닌 반복자의 일종으로 값이 리턴되는 것은 리스트는 전체 데이터를 메모리에 모두 쌓아서 만든다는 점이고, 반복자는 필요한 원소를 필요한 시점에 생성한다는 것이다. 즉 메모리를 조금 더 아껴서 쓸 수 있다는 말이다.

map 사용해보기

map 함수는 집합에 사상할 함수 하나와 집합을 인자로 받아, map 객체를 리턴하는 함수이다. 이 때 인자로 받게 되는 함수는 집합 내의 각 원소를 변형하는 함수로 인자 하나를 받고, 값 하나를 리턴한다. (이 때 인자와 리턴값의 타입은 달라도 무방하다.) 또한 함수는 미리 정의된 함수가 있는 경우 함수의 이름을 그대로 사용하면 되고, 그렇지 않은 경우에는 리듀스 편에서도 살펴봤던 람다식을 쓸 수 있다.

예제를 통해서 map()함수를 쓰는 방법을 살펴보자.

a = range(1, 10)  # 1,2,3...,9

# 각 원소를 2배 하기
b = map(lambda x: x * 2, a)

# 맵의 결과는 반복자이므로, for 문을 통해서 순회할 수 있으며
for e in b:
    print(b)


# 맵핑한 결과를 리스트로 만드려면 list()함수를 쓴다.
c = list(b) # [2, 4, 6, 8..., 18]

filter 사용하기

필터의 사용방법도 map과 동일하다. 차이가 있다면 filter()함수는 특정한 조건을 만족하는 구성 원소만을 취하므로, 인자로 넘겨받는 함수는 변형이 아닌 "기준에 부합하는지를 평가"하는 함수가 된다. 따라서 이 함수는 a라는 임의의 값을 받아 bool 타입 값, 즉 TrueFalse를 리턴하거나 참/거짓으로 평가되는 표현식이어야 한다.

filter()의 결과는 조건으로 주어진 함수가 True를 리턴하는 원소만을 내놓게 된다.

a = range(1, 10)  # 1, 2, 3,... 9

# 짝수만을 골라내기
b = filter(lambda x : x % 2 is 0, a)  # 2, 4, 6, 8

# 5보다 큰 수만을 골라내기
c = filter(lambda x: x > 5, a) # 6, 7, 8, 9

리스트 축약

리스트처럼 원소들이 자신만의 순서를 가지고 나열된 컨테이너를 파이썬에서는 연속열(Sequence)라는 개념으로 부른다. (물론 Sequence라는 타입이나 프로토콜을 명시적으로 정의하지는 않았으나, 통용되는 개념이라 보면 된다.) 연속열을 다른 리스트 혹은 반복자로 변환할 때는 map(), filter() 함수를 쓸 수 있는데, 보통은 리스트 축약 문법을 이용해서 변환하기도 한다.

그렇다고 해서 맵/필터가 의미없는 것은 아니다, 앞에서도 설명했지만 이들 함수는 한 번에 리스트를 통째로 변환하는 것이 아니라, 반복자를 생성하여 변환이나 필터링을 각 원소를 사용하는 시점에 적용하는 느긋한(lazy) 방식으로 동작한다. 따라서 쓸데없이 메모리를 미리 사용하지 않는다는 장점이 분명 존재한다.

리스트 축약 문법은 말하자면 수학에서 집합을 조건 제시법을 통해서 나타내는 것과 동일하다. 만약 a라는 리스트가 한자리의 자연수라 하면 다음과 같이 나타낼 수 있다.

  • a = [1, 2, 3, 4, 5, 6, 7, 8, 9] ➡️ 원소나열법
  • a = [ x | x ∋ N, x < 10 (N은 자연수)] ➡️ 조건 제시법

실제 파이썬에서는 자연수 전체의 집합이라는 무한집합은 정의할 수 없지만, 크기가 유한한 모집합이 있는 경우에는 이런 방식으로 쓸 수 있다.

# 리스트 축약 문법
[ {각원소를 위한 표현식}  for {변수} in {모집합}  (if {변수의 조건})]

  1. 모집합 : 새로운 리스트를 만들기 위한 베이스가 되는 원래의 리스트를 의미한다.
  2. for 변수 in : 모집합의 각 원소를 참조하는 변수를 임시로 설정한다.
  3. 각 원소를 위한 표현식 : for 문에서 정의한 변수를 다시 표현식으로 변환할 수 있다. map에 해당한다.
  4. if 변수의 조건 : filter 조건에 해당한다.

사실 예제로 보는게 더 이해가 쉽다. 참고로 리스트 축약은 for 반복문이 아니다. 잊지 말자.

a = range(1, 10)  # 1: range 객체로 1, 2, 3..., 9
b = [ x for x in a ]  #2 반복자를 그대로 리스트로 만들었다.
c = [ x * 2 for x in a]  #3 반복자의 각 원소를 2배한 리스트를 만들었다. 
d = [ x for x in a if x % 2 == 0]  #4 반복자의 원소가 짝수일 때로 필터링한다. 

# 맵과 필터를 한 문장으로 조합할 수 있다.
# 다음 식은 반복자의 원소가 홀수 일때만 이를 두 배한 원소를 취해서 새 리스트를 만든다. 
e = [ x * 2 for x in a if x % 2 == 1] 

활용

리스트 축약은 리스트를 반복문 내에서 조작하는 코드를 엄청나게 간단하게 줄여준다. 예를 들어서 키보드로부터 공백으로 분리된 정수 10개를 입력받아, 그 합을 구하는 프로그램을 작성한다고 가정해보자. 플레인한 코드는 다음과 같이 작성할 것이다.

words = input().split(' ')
array = []
for e in words:
    n = int(e)
    array.append(n)
pritn(sum(array))

이 코드는 입력받은 문자열을 공백으로 쪼개고, 각 단어에 해당하는 문자열을 정수로 바꾼 후, 결과 리스트에 하나씩 추가했다.

이 동작을 리스트 축약을 써서 구현하면 다음과 같다.

array = [int(x) for x in input().split(' ')]
print(sum(array))

리스트 축약은 이처럼 코드를 비약적으로 줄이며, 리스트에 대해서 append()를 반복적으로 호출하지 않으므로 성능면에서도 훨씬 빠르다. 또한 하나의 구문으로 맵/필터를 동시에 적용할 수 있다. 예를 들어 위 코드에서 만약 숫자가 아닌 문자가 입력된다면 에러가 날 것이다. 따라서 이 부분은 각각의 단어가 숫자인지 여부를 검사하여 필터링하면 된다. 따라서 극단적으로는 아래와 같이 단 한줄로 해결이 되기도 한다.

print(sum([int(x) for x in input().split() if x.isdigit()]))

보너스

리스트 축약은 중첩도 가능하다. 다음 한 줄의 코드는 구구단을 2~9단까지 출력하는 코드이다.

pprint('\n\n\n'.join('\n'.join('{:02d} X {:02d} = {:02d}'.format(x, y, x*y) for y in range(2, 10)) for x in range(2, 10)))

물론 이렇게 하는 것이 가능하다는 것을 보여주는 예시일 뿐, 중첩 축약과 같은 표현은 실질적으로는 코드의 가독성을 매우 낮출 수 있기 때문에 적극적으로 권장하지는 않는다. 하지만 여러분이 인터넷에서 볼 수 있을 엄청나게 많은 파이썬 코드들은 이러한 표현을 적극적으로 사용하고 있을 것이며, 여러분 역시 이 문법은 숨쉬듯 자연스럽게 느껴질 때까지 많은 연습을 해볼 것을 권한다.



1 커피 자판기를 생각하면 된다. 이때 자판기는 동전을 커피로 바꾸는 함수이다. (f = ⚫ ➡️ ☕)
2 물론 수학에서의 함수와 파이썬의 함수가 완전히 일치하는 개념은 아니다. 수학에서의 함수는 입력값이 같으면 반드시 그 출력값이 같다. 이를 함수의 순수성이라고 하는데, 프로그래밍 언어 내의 함수는 변할 수 있는 외부의 값을 참조하는 것이 가능하므로 그렇지 않는 경우가 발생할 수 있으니 주의한다


반응형

'파이썬 how to' 카테고리의 다른 글

파이썬 사전 사용법  (0) 2017.05.19
파이썬 튜플 기초 사용법  (0) 2017.05.15
iPython 설치하는 방법  (3) 2017.05.09
Reduce 사용법, 그리고 lambda  (0) 2017.04.27
파이썬 리스트의 기본 사용법  (0) 2017.04.25
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함