functional python에 대한 단상

문득, 이런 생각이 들었다.

temp = []
for i in range(10):
  temp.append(i*i)

이 코드는 10보다 작은 완전제곱수의 리스트를 만드는 함수다. 빈 리스트를 만들고 range() 로 부터 값을 받아 제곱한 다음, 리스트에 넣는다. 이 과정은 파이썬에 익숙한 사람이라면 반복문 보다는 리스트 축약으로 표현할 것이다.

temp = [i*i for i in range(10)]

파이썬 리스트 축약의 기본 컨셉은 어떤 연속열로부터 다른 리스트를 만드는 것이고 이것은 람다식을 연속열에 사상(mapping over)하는 것이다.  다른 예로, 사용자로부터 공백으로 구분되는 숫자들을 받아서 정수 리스트를 만들고자 한다면, 공백으로 나눠진 단어(str)들에 int( )를 맵핑하여 정수 리스트를 만들 수 있을 것이다. 흔히 온라인 저지에서 문제를 풀 때 이런식으로 숫자값들을 받을 수 있다.

xs = [int(n) for n in input().split()]

그러면 다시, 처음으로 돌아와보자. 이렇게 입력을 받는 경우에 부득이하게 반복문을 쓰면서 리스트에 값을 추가해야 하는 경우가 있다. 바로 입력의 수가 정해지지 않는 경우이다. 예를 들어 빈 줄이 입력될 때까지 문장을 입력받은 후, 입력된 문자열들을 모두 대문자화해서 출력하는 코드를 작성한다면 다음과 같은 식이 될 거다.

lines = []
while True:
  line = input()
  if not line:
    break
  lines.append(line)

for line in lines:
  print(line.upper())

그런데 이 코드의 상단은 반복문에서 리스트에 원소값을 추가해 나가는 식이기 때문에 왠지 리스트 축약으로 바꾸고 싶은 생각이 든다. 이게 가능할까? 다시 제곱수를 만드는 리스트 축약 구문으로 돌아와 생각해보자.

temp = [i*i for i in range(10)]
                     ~~~~~~~~~

리스트 축약은 연속열에 대한 맵핑의 컨셉이라고 했다. 이 관점에서 range(10) 은 일종의 연속열이다. 기술적으로 이는 반복자/제너레이터인데, 개념상의 타입은 [int] 인 셈이다. 즉 거시적인 관점에서 range() 함수를 호출한 표현식은 정수의 집합 혹은 유사리스트(psuedo list)로 볼 수 있는 셈이다.

사용자로부터 입력을 받는 input() 함수의 호출표현은 그 자체로 표현식(expression)이며 str 타입으로 평가된다. 다시 제너레이터는 생성하는 값의 유사리스트로 평가될 수 있으므로, 마치 range() 처럼  [str] 타입으로 평가가능한 제너레이터를 만들 수 있지 않을까?

def get_lines():
  while True:
    line = input()
    if not line:
      raise StopIteration
    yield line

이 제너레이터는 매번 값을 요청받을 때마다 input() 의 결과를 내놓으며 더 이상 내놓을 값이 없을 때 (사용자가 빈 줄을 입력했을 때) 중지된다. 그러면 처음에 생각했던 [str] 타입의 제너레이터가 되는 셈이다.

그렇다면 다음과 같이 작성할 수 있는 것이다.

print('\n'.join(line.upper() for line in getLines()))

이 코드의 의의는, 단순히 입력받은 여러 줄을 대문자화하는 프로그램을 one-liner로 작성했다는데 있는것이 아니다. 바로 데이터의 흐름을 처리하는 방식을 바꿨다는 것이다.

처음의 프로그램은 다음과 같이 동작을 설명할 수 있다.

  1. 각 라인을 저장할 버퍼를 준비한다.
  2. 사용자가 입력한 라인을 버퍼에 추가한다.
  3. 사용자가 입력한 라인이 빈줄이 될 때까지 2를 반복한다.
  4. 버퍼에서 한 줄을 꺼내서 대문자화 한 다음 출력한다.
  5. 버퍼가 빌 때까지 4를 반복한다.

프로그램 자체가 간단하기 때문에 동작을 설명하는 내용이 어렵지는 않다. 하지만 후자의 코드는 다음과 같이 설명된다.

빈줄이 입력될때까지 들어온 라인의 리스트를 얻는다 -> 대문자화하는 변환을 맵핑한다 -> 각 라인을 개행으로 결합한다 -> 출력한다.

즉 모든 과정이 하나의 처리라인으로 직렬화되고, 맵핑, 결합, 출력이라는 단순 동작의 파이프라인으로 설명된다. 즉 프로그램 전체가 하나의 함수이며, 이는 맵핑, 결합, 출력의 함수들을 합성한 결과와 동일하다. 나는 바로 이 것이 함수형 프로그래밍 패러다임이 시사하는 점이라는 생각이 든다. 단순히 기술적인 맵, 필터, 리듀스나 1등시민으로서의 함수가 아니라 문제를 해결하는 과정 자체가 하나의 입력과 출력을 연결하는 사이에 있는 함수이고, 그 함수를 다른 함수들의 연산으로 만들어 처리하는 과정을 보이는 것이다.

기술적으로 range() 함수가 생성하는 제너레이터는 “느긋한 정수의 연속열”로 볼 수 있다. 그외의 기본함수 filter(), map() 의 결과로 생성되는 객체 역시 파이썬3에서는 더 이상 리스트가 아닌 제너레이터이며 이들도 ‘느긋하게 평가되는’ 리스트처럼 쓰인다. 비슷하게 여기서 작성한 getLines() 는 ‘빈 문자열이 들어올때까지’ 입력되는 문자열들의 느긋한 시퀀스이며, 타입상 문자열의 리스트로 평가되기 때문에 리스트 축약을 비롯한 제너레이터가 사용될 수 있는 모든 구문에 활용이 가능하다.

이 포스트에서 소개한 예제는 기술적으로 아무런 문제가 없고, 실행도 잘 된다. 이는 ‘아직 만들어지지 않은 리스트를 이렇게 다룰 수 있다’는 것을 보이는 셈인데, 느긋한 평가의 개념이 없던 프로그래밍 언어의 모델에 익숙한 사람에게 이것은 “코드의 실행 순서가 이상하다”는 느낌을 줄 수 있다. 하지만 절차에 따라 매 단위 데이터를 조작하기 보다는 입력과 출력 사이의 흐름을 보는 형태로 프로그램을 디자인하는 훈련을 해보는 것이 필요하다고는 생각한다.