콘텐츠로 건너뛰기
Home » Python 101 : 맵, 필터와 반복문

Python 101 : 맵, 필터와 반복문

지난 글에서는 리스트 및 그와 비슷한 집합 형태의 자료형을 살펴보면서 “반복 가능”이라는 개념에 대해서 소개했다. 반복 가능하다는 특성은 꼭 리스트가 아니어도 여러 개의 값을 포함하는 집합/모임(collection)의 성질을 갖는 다양한 자료형에서 나타나는 성질이며, 이러한 특성을 갖는 객체들은 그 내부 구현에 상관없이 for 루프나 comprehension 축약 문법에 적용될 수 있다.

이러한 반복 가능 객체가 가지고 있는 여러 개의 값을 다루는 방법을 생각해 본다면 for 루프를 사용하는 것을 먼저 떠올릴 수 있다. 애초에 파이썬의 for 루프는 “이터레이터”라는 특수한 타입에 맞물려서 돌아가도록 디자인되어 있기 때문인데, 이터레이터와 for 문이 어떤 식으로 돌아가는지를 좀 더 자세하게 설명한 예전 포스팅이 있으니, 관심있는 분들은 한 번 읽어보면 되겠다.

그런데, 많은 경우 이런 반복 가능 객체를 기준으로 어떤 처리를 하는 것은 하나의 반복 가능 객체를 다른 반복가능 객체나 연속열로 변경하는 작업이 주를 이룰 것이다. 실제 간단한 예제를 통해서 이 개념을 살펴보자.

정수 n을 입력받아서 * 를 사용해서 n 줄의 삼각형을 출력하는 예를 생각해보자.

input: n = 3
output:
*
**
***

input: n = 5
*
**
***
****
*****

간단한 문제이고, 보통 초급 강좌나 교재는 이런 예제를 많이 사용하기 때문에, 파이썬을 시작한지 얼마 되지 않은 사람도 이 문제는 풀 수 있을 것이다. 보통 지식인 같은 곳에서 흔히 볼 수 있는 초심자들의 답안은 대충 이런 식인데, 매우 절차지향적으로 문제를 접근하는 것을 볼 수 있다. (이렇게 푸는 답안이 나쁘다는 것이 아니다.)

n = int(input('n = '))
for i in range(n):
  for j in range(i + 1):
    print('*', end='')
  print()

그런데 이 문제를 데이터를 중심으로 다시 생각해보자. 먼저 입력 받은 문자열이 글자 ‘3’ 이라고 하자. int() 함수를 사용하면 이 글자를 정수값 3으로 변환할 수 있다. 이 것을 3개의 값으로 만들어야 하는데, 이럴 때 사용할 수 있는 함수는 range()이다. range(3) 하면 0, 1, 2 세개의 정수를 얻게 되는데, 이 각각에 (+ 1) 변형을 가하면 1, 2, 3 을 얻을 수 있다.

문자열은 그 자체로 연속열이므로 길이를 곱해서 원하는 길이의 문자열을 만들 수 있다. 즉 '*' * 2 == '**' 가 된다. 따라서 별표 문자에 1, 2, 3을 각각 곱하면 각 라인에서 출력해야 하는 문자열을 만들 수 있을 것이다. 그렇게 해서 지금까지 얻는 데이터는 ['*', '**', '***'] 이 된다. 이 것을 for 문을 통해서 출력해도 되지만, '\n'.join() 을 사용하면 다시 문자열의 리스트를 하나의 문자열로 합치는 것이 가능하다.

이 모든 과정을 하나의 변환으로 본다면 앞서 설명한 과정은 정수 3 으로 출력해야할 삼각형을 표현하는 문자열들로 만든 것이다. 말로 설명하면 길었지만, 이 과정에서 실제로 for 루프를 돌 필요는 없다. 정수를 정수의 집합으로, 다시 각 정수를 문자렬로 변환하는 처리를 했을 뿐이다. 그리고 파이썬에 조금 익숙해진 사람이라면 리스트 축약 문법을 사용하여 range 객체를 문자열의 리스트로 변형할 수 있을 거라는 생각은 어렵지 않게 할 수 있다.

자 이 생각의 과정을 코드로 다시 옮겨 보자.

n = int(input('n = '))
lines = ['*' * (i + i) for i in range(n)]
print('\n'.join(linse))

대체로 중간에 어떤 조건에 따라서 루프를 중단해야 하는 필요가 있지 않은 이상, 거의 대부분의 연속열과 관련된 처리는 for 문이 아닌 리스트 축약을 통해 할 수 있다. 이 때 중요한 것은 for 루프를 사용하지 말자는 것이 아니라, 생각의 흐름을 값이 변환되어 가는 과정에 초점을 맞추는 것이다.

리스트의 개별 원소를 변환할 수 있는 로직이 있다면, 그것을 하나의 단위로 두고 리스트의 각 원소에 적용하여 리스트를 원하는 형태에 가깝게 변환할 수 있다. 이러한 연산을 사상(맵핑, mapping)이라 한다고 지난 번에 소개한 바 있을 것이다. 이 개념을 내재화하는 것은 파이썬 문법이나 특정한 라이브러리를 잘 쓰는 것보다 가장 중요하다.

파이썬에서 파이썬스러운(pythonic) 코드를 작성하는 것은 중요한데, 파이썬스러움에서 가장 중요한 미덕은 바로 ‘읽기 쉬움’이다. 그리고 읽기 쉽다는 것은 전체로직을 한 눈에 파악하기 쉽다는 것을 의미하기도 하므로 결국 프로그램의 기능은 정해진 순서에 따라 값이 변환되어 가는 과정으로 생각해 낼 수 있다면, 좋은 코드를 작성하기도 유리하고 그만큼 복잡한 문제도 어렵지 않게 접근할 수 있다.

range 객체

for 문에 대한 글을 찾아보면 빠지지 않는 함수가 있는데, 바로 range() 함수이다. 이 함수는 정해진 범위의 정수 순열을 생성한다.

  • range(n) : 0, 1, 2, … , n-1 까지 n 개의 정수 순열을 표현하는 반복가능 객체를 만든다.
  • range(start, end, step=1) : start 부터 end-1 까지의 정수순열을 만든다. 이 때 공차는 step이다.

아주 오래전 python2에서는 range() 함수는 리스트를 리턴했지만, 지금은 range 타입이라는 별도의 객체를 만들어 낸다. 이 range 객체는 일종의 반복 가능 객체인데, 왜 이렇게 바뀌었을까? 가장 큰 이유는 메모리 사용량이다. 만약 range(1_000_000) 을 실행할 때 range()가 리스트를 만들어낸다면 어떤 일이 생길까? 파이썬은 컴퓨터 메모리에서 일단 정수 객체가 들어가 수 있는 메모리 공간 1백만개를 확보해야 한다. 그리고 0부터 999,999까지 백만개의 정수 객체를 생성하고 이 영역에 추가하여 리스트를 만들어 낸다.

하지만 이렇게 만든 리스트를 결국에는 최대값이나 합계 등으로 사용할 것이라면 1백만 개의 정수를 일일이 메모리에 생성해서 늘어놓을 이유는 없을 것이다.

map() 함수

“List Comprehension”은 개념적으로 어떤 리스트를 다른 리스트로 변환하는 것이라고 했다. 따라서 각각의 원소를 변환할 수 있는 연산(함수)이 있다면, 이 연산을 각각의 원소에 적용해주면 된다. 이러한 연산처리를 수학에서는 사상(mapping)이라고 하는데, 파이썬의 map() 함수가 여기에 해당한다. 0 -> '*', 1 -> '**' 과 같이 정수를 + 1 한 길이만큼의 별표 문자열을 만들어 내는 함수는 다음과 같이 작성할 수 있다. (아직 함수에 대해서 다루지 않았지만, 대충 안다고 치고…)

def make_line(n):
  return '*' * (n + 1)

map(f, xs) 함수는 첫번째 인자로 함수를 받고 두 번째 인자로 반복가능 객체를 받는다. 그리고 반복 가능 객체의 각각의 원소에 함수 f 를 적용하여 f(x)로 구성되는 새로운 반복가는 객체를 만든다. 결국 리스트 축약과 동일한 일을 한다.

n = int(input())
lines = map(make_line, range(n))
print('\n'.join(lines))

물론 리스트 축약은 자유자재로 다룰 수 있으면 정말 좋기 때문에, 이런 방법은 그냥 있나보다 정도로 알아두면 되겠다. 하지만 리스트 축약의 개념이 사실은 이런 mapping을 사용하는 것이라는 점은 이해하고 있는 것이 좋다.

리스트 축약에서 if 절은 특정한 조건을 내세워 해당 조건을 만족하지 않는 원소를 걸러내는 역할을 하고, 이는 for 반복문 내에서 if 분기문을 사용하여 어떤 원소를 제거하는 것과 같은 일을 한다. 이와 같은 일을 하는 함수는 filter(f, xs) 이며 사용법 자체는 map() 함수와 동일하다.

오늘은 이렇게 반복 가능 객체의 개념에 대해서 알아보았는데, 반복 가능 객체는 루프를 위해서 고안되었지만 그에 담긴 원리를 잘 이용하면 결국 for 루프 없이 반복문을 사용하는 것과 동일하면서 더 깔끔한 코드와 로직을 작성할 수 있다는 점도 알아보았다. 이 아이디어의 핵심은 리스트나 튜플, 세트, 사전은 모두 여러 값들을 모아놓은 집합이고. 이런 집합에 대해서는 함수를 사상하는 연산을 수행할 수 있다는 것이었다. 그러면 다음 시간에는 값을 변환하는 도구인 함수에 대해서 살펴보도록 하겠다.