본문 바로가기
Python/파이썬 프로그래밍 연습

기능적 프로그래밍

by 가므자 2012. 4. 25.

무엇을 다룰 것인가?

· 기능적 프로그래밍과 전통적 프로그래밍 스타일 사이의 차이점

· 파이썬 FP 함수와 테크닉

· 람다 함수

· 단축 회로 불리언 평가

· 표현식으로서의 프로그램

주제에서는 파이썬이 기능적 프로그래밍(FP)이라는 또다른 프로그래밍 스타일을 어떻게 지원할 있는지 살펴보겠습니다. 재귀에서와 같이 FP 정말 고급스러운 주제이므로 지금 당장은 무시하고 싶을지도 모르겠습니다. 기능적 테크닉은 일상적인 작업에 자주 사용됩니다. FP 옹호하는 사람들의 믿음에 의하면 FP야말로 소프트웨어 개발을 위한 근본적으로 좋은 방법입니다.

기능적 프로그래밍이란 무엇인가?

기능적 프로그래밍은 명령적 (또는 절차적) 프로그램과 혼동하면 안됩니다. 두 방식 모두 객체 지향 프로그래밍과 비슷하지 않습니다. 다른 것입니다. 그렇게 완전히 다르지는 않습니다. 앞으로 탐험할 개념들은 그냥 다른 방식으로 표현된 것일 뿐 친숙한 프로그래밍 개념들입니다. 문제를 해결하기 위해 이런 개념을 적용하는 방식 뒤에 숨은 철학도 약간 다릅니다.

기능적 프로그래밍은 표현식(expressions)에 관한 것입니다. 사실 또다른 방식으로 FP를 기술하면 표현식 지향 프로그래밍이라는 용어를 붙일 수 있겠습니다. FP에서 모든 것은 표현식으로 축약되기 때문입니다. 표현식은 연산과 변수의 집합으로서 결과로 단 한개의 값을 산출한다는 사실이 기억이 나실 겁니다. 그리하여 x == 5 는 불리언 표현식입니다. 5 + (7-Y) 는 산술 표현식입니다. "Hello world".uppercase()는 문자열 표현식입니다. 후자는 문자열 객체 "Hello world"에 함수를 요청한다고 하기도 합니다 (즉 엄밀하게 말해 메쏘드 호출입니다). 뿐만 아니라 앞으로 보시겠지만 FP에서 함수는 (이미 그 이름으로 짐작하시듯이!) 아주 중요합니다.

함수는 FP에서 객체로 사용됩니다. 다시 말해 함수는 프로그램 안에서 다른 변수들처럼 거의 똑 같이 이리저리 건네집니다. GUI 프로그램에서 이런 예제를 보셨습니다. 함수의 이름을 버튼 콘트롤의 command 속성에 할당했습니다. 사건 처리자 함수를 객체로 취급하고 그 함수를 가리키는 잠조점을 버튼에 할당했습니다. 함수를 이리 저리 건넨다는 이 생각이 FP를 여는 열쇠입니다.

기능적 프로그램은 대단히 리스트 지향적이기도 합니다.

마지막으로 FP는 문제 해결에 어떻게가 아니라 무엇에 초점이 있습니다. 다시 말해, 기능적 프로그램은 해결 메커니즘에 초점을 두기 보다 해결해야 할 문제를 기술해야 합니다. 이런 식으로 작동시킬 의도로 여러 프로그래밍 언어가 있습니다. 그 중에 널리 사용되는 언어는 하스켈(Haskell)입니다. 하스켈은 웹사이트(www.haskell.org)에 가면 하스켈 언어 뿐만 아니라 FP의 철학을 기술한 수 많은 논문이 있습니다. (개인적인 견해로는 이 목적이 물론 칭송받을만 하지만 약간 FP의 옹호자들에 의하여 과장되어 있다는 생각이 듭니다.)

순수하게 기능적 프로그램은 프로그램의 의도가 반영된 표현식을 정의하여 구성됩니다. (또다른 표현식 안에 둘러싸인) 표현식의 각 조건마다 이번에는 문제의 특징이 서술되고 각 조건을 평가하다 보면 결국 해결책이 도출됩니다.

, 그건 이론입니다. 과연 작동할까요? 그렇습니다. 가끔은 아주 잘 작동합니다. 문제의 유형에 따라 자연스럽고 강력한 테크닉이 될 수 있습니다. 불행하게도 많은 문제는 수학적 원리에 영향을 받아서 상당히 추상적인 사고 스타일을 요구합니다. 결과로 나온 코드는 종종 평범한 프로그래머가 읽기가 버겁습니다. 그러나 동등한 명령적 코드에 비해 훨씬 더 짧고 신뢰성도 더 높습니다.

뒤의 이 간결함과 신리성이라는 두 성질 덕분에 많은 전통적인 명령적 프로그래머나 객체 지향적 프로그래머가 FP를 연구하게 되었습니다. 온 마음으로 받아 들이지는 못할지라도 모두가 유용하게 사용할 수 있는 강력한 도구들이 여럿 있습니다.

FP 그리고 신뢰성

기능적 프로그램의 신뢰성은 부분적으로 FP 구조와 Z VDM 같은 형식 규격 언어 사이에 관계가 아주 밀접하기 때문입니다. 눈에 보이는 그대로 그 규격을 하스켈 같은 FP 언어로 번역할 수 있습니다. 물론 원래 규격이 잘못되었으면 결과 프로그램은 정확하게 그 에러를 반영할 뿐입니다!

이런 원리를 컴퓨터 과학에서 "쓰레기를 넣으면 쓰레기가 나온다(Garbage In, Garbage Out)"라고 부릅니다. 시스템 요구조건을 간결하고 명료하게 표현하는 일은 근본적으로 어렵기 때문에 소프트웨어 공학의 최대 도전과제로 남아 있습니다.

파이썬은 어떻게 하는가?

파이썬은 기능적으로 프로그래밍에 접근할 수 있도록 여러가지 함수를 제공합니다. 이런 함수들은 모두 파이썬으로 아주 쉽게 작성이 가능하다는 점에서 편의를 위해 제공되는 특징입니다. 그렇지만 더 중요한 것은 그의 묵시적인 의도입니다. 즉 파이썬 프로그래머에게 원한다면 FP 방식으로 작업할 수 있도록 허용하는 것입니다.

제공된 함수들을 몇 가지 살펴보고 다음과 같이 정의한 샘플 데이터 구조에 어떻게 작동하는지 알아보겠습니다:

spam = ['pork','ham','spices']
numbers = [1,2,3,4,5]
def eggs(item): 
    return item
map(aFunction, aSequence)

이 함수는 aSequence의 원소에 aFunction 파이썬 함수를 적용합니다. 다음 표현식은:

L = map(eggs, spam)
print L

L에다 새로운 리스트를 돌려줍니다 (이 경우 spam과 동일).

다음과 같이 작성해도 같은 일을 할 수 있습니다:

for i in spam:
   L.append(i)
print L

그렇지만 주목하세요. map 함수 덕분에 코드 블록을 내포시키지 않아도 됩니다. 한 가지 관점에서 보면 그 덕분에 프로그램의 복잡도가 한 수준 낮아집니다. 그것이 FP의 테마로 자주 등장하는 것을 보게 될 것입니다. , FP 함수를 사용하면 블록이 제거됨으로써 상대적으로 코드의 복잡도가 줄어듭니다.

filter(aFunction, aSequence)

이름으로 짐작하듯이 filter함수가 True를 돌려주면 그 원소를 연속열에서 추출합니다. 숫자로 구성된 리스트를 생각해 보세요. 오직 홀수로만 구성해서 새로 리스트를 만들고 싶다면 다음과 같이 생산할 수 있습니다:

def isOdd(n): return (n%2 != 0) # mod 연산자 사용
L = filter(isOdd, numbers)
print L

다른 방식으로 작성한다면:

def isOdd(n): return (n%2 != 0)
for i in numbers:
   if isOdd(i):
      L.append(i)
print L

역시 관례적인 코드로 같은 결과를 얻으려면 두 수준의 들여쓰기가 필요합니다. 들여쓰기가 증가하는 것은 코드의 복잡도가 증가한다는 표시입니다.

reduce(aFunction, aSequence)

reduce 함수는 그의 의도가 약간 불분명합니다. 이 함수는 공급된 함수를 통하여 원소들을 결합하여 리스트를 단 한개의 값으로 줄여줍니다. 예를 들어 리스트의 값을 합산하여 총계를 다음과 같이 돌려줄 수 있습니다:

def add(i,j): return i+j
print reduce(add, numbers)

앞과 같이 좀 관례적으로 다음과 같이 할 수도 있습니다:

res = 0
for i in range(len(numbers)): # 지표화 사용
   res = res + numbers[i]
print res

이 경우 같은 결과를 산출하지만 언제나 그렇게 눈에 보이는 그대로 이해가 되는 것은 아닙니다. reduce실제로 하는 일은 공급된 함수를 연속열의 앞 두 원소를 건네어 호출한 다음 그 결과를 교체하는 것입니다. 다른 말로 해서 더 정확하게 표현하면 다음과 같습니다:

def reduce(numbers):
  L = numbers[:] # 사본을 만든다
  while len(L) >= 2:
     i,j = L[0],L[1] # 터플 할당 사용
     L = [i+j] + L[2:]
  return L[0]

한 번 더 코드의 복잡도를 감소시키는 FP 테크닉을 보고 있습니다. 코드 블록을 들여쓰기할 필요가 없습니다.

람다(lambda)

지금까지 예제에서 눈치채셨을 한 가지 특징은 FP 함수에 건네지는 함수들이 아주 짧은 경향이 있으며, 종종 한 줄 짜리 코드라는 것입니다. 파이썬은 아주 작은 함수를 수 없이 선언하는 수고를 덜기 위하여 FP에 람다(lambda)를 제공합니다. 람다라는 이름은 람다 대수학(lambda calculus)이라는 수학의 한 갈래에서 유래했습니다. 람다 대수학도 그리스 문자 람다를 이용하여 비슷한 개념을 표현합니다.

람다는 익명 함수를 가리키는데 사용되는 용어입니다. 람다는 이름은 없지만 마치 함수처럼 실행될 수 있는 코드 블록입니다. 람다는 프로그램 안에 파이썬 표현식이 놓일 수 있는 곳이면 어디든지 정의할 수 있습니다. 다시 말해 FP 함수 안에 사용할 수 있다는 뜻입니다.

람다는 다음과 같이 생겼습니다:

lambda <매개변수리스트> : <그 매개변수를 이용하는 파이썬 표현식>

그리하여 위의 add 함수는 다음과 같이 작성할 수도 있습니다:

add = lambda i,j: i+j

lambdareduce 호출에 이용하면 정의하는 줄이 전혀 필요 없습니다. 다음과 같이:

print reduce(lambda i,j:i+j, numbers)

비슷하게 mapfilter 예제를 다음과 같이 작성할 수도 있습니다:

L = map(lambda i: i, spam)
print L
L = filter(lambda i: (i%2 != 0), numbers)
print L
지능형 리스트

지능형 리스트는 새로운 리스트를 구축하는 테크닉입니다. 하스켈로부터 빌려 왔으며 파이썬 2.0 버전에 도입되었습니다. 약간 구문이 불명확하며, 수학적 집합 표기법과 비슷합니다. 다음과 같은 모양입니다:

[<표현식> for <> in <집단> if <조건>]

아래와 동등합니다:

L = []
for value in collection:
    if condition:
        L.append(expression)

다른 FP 구조처럼 이 역시 줄이 절약되고 들여쓰기가 두 수준 절감됩니다. 실용적인 예제를 살펴봅시다.

먼저 짝수로 구성된 리스트를 만들어 봅시다:

>>> [n for n in range(10) if n % 2 == 0 ]
[0, 2, 4, 6, 8]

0-9 사이의 n 중에서 짝수(i % 2 == 0)n 값으로 구성된다는 뜻입니다.

물론 끝의 조적은 함수로 교체할 수 있습니다. 그 함수가 돌려주는 값이 파이썬이 이해할 수 있는 불리언 값이기만 하면 됩니다. 그리하여 앞 예제를 다시 보면 다음과 같이 재작성할 수 있습니다:

>>>def isEven(n): return ((n%2) == 0)
>>> [ n for n in range(10) if isEven(n) ]
[0, 2, 4, 6, 8]

이제 앞의 숫자 다섯개를 제곱한 리스트를 만들어 봅시다:

>>> [n*n for n in range(5)]
[0, 1, 4, 9, 16]

마지막의 if 절이 언제나 필요한 것은 아닙니다. 여기에서는 초기 표현식이 n*n이고 범위에 있는 값을 모두 사용합니다.

마지막으로 range 함수 대신에 기존의 집단을 사용해 봅시다:

>>> values = [1, 13, 25, 7]
>>> [x for x in values if x < 10]
[1, 7]

이는 다음의 filter 함수를 교체하는데 사용될 수 있습니다:

>>> filter(lambda x: x < 10, values)
[1, 7]

지능형 리스트는 변수 하나 또는 테스트 하나로 제한되지 않습니다. 그렇지만 변수와 테스트가 늘어날 수록 코드는 아주 복잡해지기 시작합니다.

지능형 리스트나 전통적인 함수가 여러분에게 자연스러운지 또는 적절한지 그 여부는 순전히 주관적입니다. 새로운 집단을 기존의 집단에 근거하여 구축할 때 앞의 FP 함수를 이용하거나 새로운 지능형 리스트를 이용할 수 있습니다. 완전히 새로운 집단을 만려면 보통 지능형 리스트를 이용하는 것이 더 쉽습니다.

그렇지만 이런 구조가 매력적으로 보일지라도 표현식은 원하는 결과를 얻으려면 너무 복잡해질 수 있습니다. 차라리 그냥 확장해서 전통적인 파이썬 방식으로 일하는 편이 더 쉽습니다. 그렇게 해도 부끄러울 일이 없습니다 - 가독성은 언제나 불분명한 것보다 더 좋습니다. 특히 그불분명이 그냥 똑똑한 체하기 위한 것이라면 말입니다!

다른 구조

물론 이 함수들은 그 자체로 유용합니다. 그러나 파이썬 안에서 완벽하게 FP 스타일을 지원하기에는 충분하지 않습니다. FP 접근법으로 언어의 제어 구조도 손댈 필요가 있으며 적어도 교체되어야 합니다. 이를 달성하는 방법은 파이썬이 불리언 표현식을 평가하는 방식의 부작용을 응용하는 것입니다.

단축회로 평가

파이썬은 불리언 표현식을 단축 회로 평가하기 때문에 이런 표현식의 특성을 활용할 수 있습니다. 단축 회도 평가를 요약하면: 불리언 표현식이 평가될 때 왼쪽 표현식에서 시작하여 오른쪽으로 평가가 진행되며, 더 이상 평가할 필요가 없으면 멈추어서 최종 결과를 산출합니다.

구체적인 예제를 취해 단축 회로 평가가 어떻게 작동하는지 살펴 봅시다:

>>> def TRUE():
...   print ''
...   return True
...   
>>>def FALSE():
...   print '거짓'
...   return False
...

먼저 함수를 두 개 정의합니다. 실행될 때 자신의 이름에 담긴 값을 돌려줍니다. 이제 이 함수들을 이용하여 불리언 표현식이 어떻게 평가되는지 텀험해 보겠습니다:

>>>print TRUE() and FALSE()
거짓
False
>>>print TRUE() and TRUE()
True
>>>print FALSE() and TRUE()    # 단축 평가 : FALSE()에서 거짓으로 결론
거짓
False
>>>print TRUE() or FALSE()     # 단축 평가 : TRUE()에서 참으로 결론
True
>>>print FALSE() or TRUE()
거짓
True
>>>print FALSE() or FALSE()
거짓
거짓
False

오직 AND 표현식의 앞 부분이 참(True)일 경우에만 뒷 부분이 평가됩니다. 앞 부분이 거짓(False)이면 뒷 부분은 평가되지 않습니다. 표현식이 전체적으로 참(True)될 수 없기 때입니다.

마찬가지로 OR 기반의 표현식에서 앞 부분이 참(True)이면 뒷 부분은 평가할 필요가 없습니다. 전체적으로 참(True)수 밖에 없기 때문입니다.

불리언 표현식을 평가하는 파이썬의 특징중에서 이용할 수 있는 특징이 또 있습니다. , 표현식을 평가할 때 파이썬은 단순히 TrueFalse를 돌려주지 않습니다. 오히려 그 표현식의 실제 값을 돌려줍니다. 그리하여 다음과 같이 빈 문자열을 테스트하면 (False로 간주됨):

if "이 문자열은 비여 있지 않다": print "비어 있지 않음"
else: print "문자열 없음"

파이썬은 그냥 문자열 자체를 돌려줍니다!

이런 특성을 이용하면 분기 같은 행위를 만들어 낼 수 있습니다. 예를 들어 다음과 같은 코드가 있다고 해 봅시다:

if TRUE(): print "참이다"
else: print "거짓이다"

이를 FP 스타일 구조로 교체할 수 있습니다:

V =  (TRUE() and "참이다") or ("거짓이다")
print V

예제를 따라가며 실험해 보세요. 다음 TRUE() 호출을 FALSE() 호출로 교체해 보세요. 그리하여 불리언 표현식의 단축 회로 평가를 이용하면 관례적인 if/else 서술문을 프로그램에서 제거하는 법을 방법을 찾을 수 있습니다. 재귀 주제에서 회돌이 구조를 교체하는데 재귀가 사용될 수 있다는 것을 보신 기억이 나실 겁니다. 그리하여 이 두 효과를 조합하면 프로그램에서 모든 관례적인 제어 구조를 없앨 수 있고, 순수하게 표현식으로 교체할 수 있습니다. 이는 순수한 FP 스타일 해결책으로 향하는 큰 걸음입니다.

이 모든 것을 실전에 사용하기 위해 완전히 기능적 스타일의 팩토리얼 프로그램을 def 대신 lambda, 회돌이 대신 재귀, if/else: 대신 단축 회로 평가를 사용하여 만들어 봅시다.

>>> factorial = lambda n: ( (n <= 1) and 1) or
...                       (factorial(n-1) * n)
>>> factorial(5)
120

이게 정말 다입니다. 관례적인 파이썬 코드에 비해 그렇게 지능적이진 못할지 몰라도 작동할 뿐만 아니라 순수한 표현식으로 구성된 순수하게 기능적 스타일의 함수입니다.

맺는

이 시점에서 궁금하실지도 모르겠습니다. 정확하게 이 모든 것의 요점이 무엇인가? 여러분만 그런 것이 아닙니다. FP가 많은 컴퓨터 과학도에게 (종종 수학도에게도) 매력적이지만 대부분의 실전 프로그래머들은 FP 테크닉을 자제해서 사용합니다. 사용하더라도 적절하다고 느끼는대로 전통적인 명령적 스타일과 섞어서 일종의 하이브리드 방식으로 사용합니다.

리스트의 원소에 연산을 적용해야 한다면 map이나 reduce 또는 filter가 해결책을 표현하는 자연스러운 방법처럼 보이고 기어이 그 함수들을 사용합니다. 아주 가끔씩 전통적인 회돌이보다 재귀가 더 적절한 경우도 있습니다. 관례적인 if/else에 비해 단축 회로 평가가 사용될 경우는 아주 드물 것입니다 - 특히 표현식 안에서 필수적인 경우를 제외하고 말입니다. 다른 프로그래밍 도구처럼 철학에 얽매이지 마세요. 오히려 과업에 가장 적절하게 손에 맞는 도구를 사용하세요. 적어도 대안이 존재한다는 사실은 배웠습니다!

람다에 관하여 언급할 마지막 요점이 있습니다. FP 영역을 벗어나 람다는 실제 사용되는 곳이 있습니다. GUI 프로그래밍에서 사건 처리자를 정의하는데 사용됩니다. 사건 처리자는 종종 아주 함수가 짧거나 또는 직접 몇 가지 인자 값을 가지고 단순히 더 큰 함수를 호출할 뿐입니다. 어느 경우든 람다 함수는 사건 처리자로 사용됩니다. 그러면 수 많은 자잘한 함수들을 정의할 필요가 없으며 겨우 한 번만 사용될 이름 공간을 이름으로 채우지 않아도 됩니다. 람다 서술문은 함수 객체를 돌려줍니다. 이 함수 객체는 위젯에 건네지고 사건이 일어나면 호출됩니다. Tkinter에서 버튼 객체를 어떻게 정의했는지 기억이 나신다면, 람다로는 이렇게 표현될 것입니다:

def write(s): print s
b = Button(parent, text="Press Me", 
           command = lambda : write("I got pressed!"))
b.pack()

물론 이 경우 기본 매개변수 값을 write()에 할당하고 writeButtoncommand 값으로 할당하기만 하면 같은 일을 할 수 있습니다. 그렇지만 여기에서도 lambda 형태를 사용하면 장점이 있습니다. write() 함수 만으로 이제 여러 버튼에 사용될 수 있습니다. 그냥 다른 문자열을 lambda를 이용하여 건네면 됩니다. 그리하여 두 번째 버튼을 이렇게 추가할 수 있습니다:

b2 = Button(parent, text="Or Me", 
           command = lambda : write("So did I!"))
b2.pack()

사건 객체를 인자로 보내는 엮기 테크닉을 사용할 때 lambda를 채용할 수도 있습니다:

b3 = Button(parent, text="Press me as well")
b3.bind(<Button-1>, lambda ev : write("Pressed"))

, 람다는 진정으로 기능적 프로그래밍을 위한 것입니다. 더 깊이 보고 싶다면 다른 자원이 많습니다. 그 중에 몇 개는 아래에 나열하였습니다. VBScript JavaScript 모두 직접적으로 FP를 지원하지는 않지만 완고한 프로그래머라면 기능적 스타일로 사용할 수 있습니다. 핵심 열쇠는 프로그램을 표현식으로 구성하고 다른 변수들을 수정하는 부작용을 허용하지 않는 것입니다.

다른 자원

· IBM 사이트에 파이썬 FP에 관한 데이비드 머츠(David Mertz) 박사의 탁월한 글이 있습니다다. 제어 구조에 관하여 깊이 연구하며 그 개념을 예제로 자세하게 설명합니다.

· 다른 언어는 파이썬보다 FP를 더욱 더 잘 지원합니다. 예를 들면: Lisp, Scheme, Haskell, ML 그리고 기타 등등이 있습니다. 특히 하스켈 사이트에는 FP에 관한 정보가 풍부합니다.

· comp.lang.functional이라는 뉴스 그룹도 있습니다. 거기에 가면 최신 소식을 접할 수 있으며 유용한 FAQ를 볼 수 있습니다.

· 여러 참조서를 위의 참조 사이트에 가면 볼 수 있습니다. 한 가지 고전 책은 아벨만(Abelman)과 수스만(Sussman) 부부가 집필한 컴퓨터 프로그램의 구조 & 통역입니다. 이 책은 전적으로 FP를 다루지는 않지만 그 원리는 잘 설명하고 있습니다. 이 텍스트는 Lisp의 확장 버전인 Scheme에 초점을 둡니다. 본인이 참조하는 주요 소스는 폴 허닥(Paul Hudak)이 집필한 The Haskell School of Expression으로서 당연히 하스켈에 관한 책입니다.

누구든지 좋은 참조서를 발견하면 아래의 링크를 통하여 이메일 한 통을 부탁드립니다.

기억해야 할 것

· 기능적 프로그램은 순수한 표현식이다.

· 파이썬은 mapfilter 그리고 reduce 뿐만 아니라 지능형 리스트도 제공하여 FP 스타일 프로그래밍을 지원한다.

· 람다(lambda) 표현식은 익명 (이름없는) 코드 블록으로서 변수에 할당하거나 함수로 사용할 수 있다.

· 불리언 표현식은 오직 결과를 보증하기 위하여 필요한 범위까지만 평가된다. 이 특성을 제어 구조에 이용할 수 있다.

· 파이썬의 FP 특징을 재귀와 조합하면 파이썬에서 FP 스타일로 거의 어떤 함수라도 작성할 수 있다 (그러나 보통 권장하지 않는다).

 

출처 : http://coreapython.hosting.paran.com/tutor/index.htm

댓글