본문 바로가기

Job Notes/Programming

[펌] Charming Python: Python에서의 함수 프로그래밍, Part 2

Charming Python: Python에서의 함수 프로그래밍, Part 2

함수 프로그래밍 시작하기


JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.


난이도 : 초급

David Mertz, Ph.D., Applied Metaphysician, Gnosis Software, Inc

2001 년 4 월 01 일

David Mertz는 Part 1 에 이어서 FP(Functional Programming)에 대한 기본 개념을 설명하고 있다. David는 다양하고 진보적인 FP 개념들을 설명하면서 프로그램 문제 해결에 대해 다른 패러다임을 제시한다.

Part 1 에서 FP에 대한 기본 개념을 설명했다. 이번 칼럼에서는 다양한 개념들을 좀 더 깊게 연구하려고 한다. Bryn Keller의 "Xoltar Toolkit"에서 많은 부분 도움을 받을 것이다. Xoltar Toolkit에는 functional 모듈과 lazy 모듈이 있다. lazy 모듈은 "필요할 때에만" 검사(evaluation)를 수행하는 구조를 지원한다. 많은 기존의 함수 언어들도 lazy evaluation을 가지고 있기 때문에 Xoltar Toolkit으로 Haskell과 같은 함수 언어에서 얻어낼 수 있는 많은 부분을 수행할 수 있다.l.

바인딩 (Bindings)

주의 깊은 독자라면 Part 1에서 지적한 함수 기술의 한계를 기억할 것이다. 특별히 Python에서는 함수의 수식을 나타내는 데에 사용하는 이름의 리바인딩(rebinding) 을 막지 않는다. FP에서 이름들은 일반적으로 긴 수식의 약자로 이해된다. 그리고 그 약속에는 "같은 수식이 항상 같은 결과를 검사한다" 라는 의미가 함축되어있다. 이름들이 리바인딩 되면 그 약속은 깨진다. 예를 들어 함수 프로그램에서 다음과 같이 단축형 수식을 정의한다고 가정해 보자.:


Listing 1. Python FP session: 오류를 범할 수 있는 리바인딩

>>> car = lambda lst: lst[0]
>>> cdr = lambda lst: lst[1:]
>>> sum2 = lambda lst: car(lst)+car(cdr(lst))
>>> sum2(range(10))
1
>>> car = lambda lst: lst[2]
>>> sum2(range(10))
5

불행하게도, 수식 자체는 변하기 쉬운 변수를 사용하지 않는다 해도 같은 수식인 sum2(range(10))은 두 부분에서 두개의 다른 결과를 검사하게 된다.

functional 모듈은 Bindings 라는 클래스를 제공한다. Bindings 는 그와 같은 리바인딩을 방지한다. Bindings를 사용할 때에는 별도의 신택스가 필요하긴 해도 리바인딩은 방지할 수 있다. functional 모듈에 있는 예제에서 Keller는 Bindings 인스턴스를 let 으로 명명한다. 다음의 예제를 보자.:


Listing 2. Python FP session: 리바인딩 방지

>>> from functional import *
>>> let = Bindings()
>>> let.car = lambda lst: lst[0]
>>> let.car = lambda lst: lst[2]
Traceback (innermost last):
  File "<stdin>", line 1, in ?
  File "d:\tools\functional.py", line 976, in __setattr__
    raise BindingError, "Binding '%s' cannot be modified." % name
functional.BindingError:  Binding 'car' cannot be modified.
>>> car(range(10))
0

분명히 실제 프로그램에서는 "BindingError"들을 잡기위해 어떤 조치를 취해야 한다. 그럼으로써 문제를 해결할 수 있다.

Bindings 와 같이 functional 은 Bindings 인스턴스에서 namespace를 빼내는 namespace 함수를 제공한다. 이것은 Bindings에서 정의된 (변하지 않는) namespace안에서 수식을 수행하고자 할 때 유용하게 쓰일 수 있다. Python 함수 eval()를 이용하면 namespace안에서 검사(evaluation)가 가능하다. 예제를 보면 쉽게 이해할 수 있을 것이다.:


Listing 3. Python FP session: 변하지않는 namespace 사용하기

>>> let = Bindings()      # "Real world" function names
>>> let.r10 = range(10)
>>> let.car = lambda lst: lst[0]
>>> let.cdr = lambda lst: lst[1:]
>>> eval('car(r10)+car(cdr(r10))', namespace(let))
>>> inv = Bindings()      # "Inverted list" function names
>>> inv.r10 = let.r10
>>> inv.car = lambda lst: lst[-1]
>>> inv.cdr = lambda lst: lst[:-1]
>>> eval('car(r10)+car(cdr(r10))', namespace(inv))
17




위로


Closures

FP에는 closure 라는 재미있는 개념이 있다. 사실 closure 는 개발자들에게는 충분히 매력적인 개념이다. 그래서 Perl이나 Ruby 같은 비함수 언어도 closure 개념을 포함시켜가는 추세이다. 게다가 Python 2.1에는 어휘 유효 범위(lexical scoping)가 추가될 예정이다.

그렇다면 closure는 무엇인가? Steve Majewski는 최근에 Python 뉴스 그룹에 개념을 훌륭하게 정의해 놓았다 :

OOP에서의 객체가 하이드라면, FP의 closure는 지킬박사와 같다. (또는 역할이 반대가 될 수도 있다). 객체 인스턴트 처럼, closure는 데이터들과 함수들을 한데 묶어서 수행하는 방식이다.

객체들과 closure 들이 문제를 어떻게 해결하는지 보자. 또한 그 두개 없이 문제들이 어떻게 해결되는지 보자. 일반적으로 함수에 의해 리턴 된 결과는 계산에 사용된 context에 의해 결정된다. 이러한 context를 정하는 가장 일반적이고 확실한 방법은 몇몇 인자를 함수로 전달하여 무엇을 처리해야 하는지 알려주는 것이다. "background"와 "foreground" 인자는 자연스럽게 구별된다. 다시 말해서 함수가 특정 시간에 어떤 함수를 실행하고 있는 지와 함수가 다중의 잠재적인 호출을 위해 "설정되는(configured)" 방식이 구별된다.

foreground 에 집중하면서 background를 핸들 할 수 있는 많은 방법들이 있다. 단순히 모든 호출에 대해 "인내심을 가지고" 함수가 필요로 하는 모든 인자를 전달하는 것이다. 이것은 값이 체인(chain) 어딘가에서 요구되어질 경우 많은 값들을 (또는 다중 슬롯 구조) 전체 호출 체인으로 전달하게 되는 결과를 초래한다. 간단한 예제를 보자.:


Listing 4. Python FP session: cargo 변수 보여주기

>>> def a(n):
...     add7 = b(n)
...     return add7
...
>>> def b(n):
...     i = 7
...     j = c(i,n)
...     return j
...
>>> def c(i,n):
...     return i+n
...
>>> a(10)     # Pass cargo value for use downstream
17

cargo 예제에서 b() 안에 있는 n은 c()로 전달 될 수 있다는 것 이외에 다른 목적은 없다. 다른 옵션은 global 변수를 사용하는 것이다:


Listing 5. Python FP session: global 변수

>>> N = 10
>>> def addN(i):
...     global N
...     return i+N
...
>>> addN(7)   # Add global N to argument
17
>>> N = 20
>>> addN(6)   # Add global N to argument
26

global N 은 addN()을 호출하고 싶을 때 사용할 수 있다. 하지만 global background "context"를 반드시 전달할 필요는 없다. "Python 적인" 기술은 정해진 시간에 디폴트 인자를 사용하여 변수를 함수 안으로 "freeze" 하는 것이다. :


Listing 6. Python FP session: frozen 변수

>>> N = 10
>>> def addN(i, n=N):
...     return i+n
...
>>> addN(5)   # Add 10
15
>>> N = 20
>>> addN(6)   # Add 10 (current N doesn't matter)
16

frozen 변수는 본질적으로 closure이다. 어떤 데이터는 addN() 함수에 "붙게(attached)" 된다. 완벽한 closure를 위해, addN()이 정의될 때 나타난 모든 데이터는 호출할 때 사용 할 수 있다. 하지만 이 예제에서는 default 인자 만으로 충분히 사용할 수 있다. addN()에 의해서 전혀 사용되지 않은 변수들은 계산에 어떤 차이점을 만들지 않는다.

좀 더 현실적인 문제들에 대해 "OOP 적인" 방식은 어떠한지 살펴보자. 다음의 "interview" 형식의 세금 계산 프로그램에서는 데이터를 모아서 (특정 순서대로 할 필요는 없다) 그것들을 모두 계산에 사용한다. 간단하게 살펴보자.:


Listing 7. Python 스타일의 tax calculation 클래스/인스턴스

class TaxCalc:
    def taxdue(self):
        return (self.income-self.deduct)*self.rate
taxclass = TaxCalc()
taxclass.income = 50000
taxclass.rate = 0.30
taxclass.deduct = 10000
print "Pythonic OOP taxes due =", taxclass.taxdue()

TaxCalc 클래스에서 (또는 인스턴스에서) 순서와 상관없이 몇 가지 데이터를 수집할 수 있다. 그리고 필요한 엘리먼트를 모두 가지면 데이터들을 계산하기 위해서 이 객체의 메소드를 호출할 수 있다. 모든 것이 인스턴스안에 함께 머물러있다. 그리고 다른 인스턴스는 다른 데이터들의 계산을 수행할 수 있다. 단지 그들의 데이터를 구별해서 다중 인스턴스를 구현하는 것은 "global variable" 또는 "frozen variable" 접근 방식으로는 가능하지 않다. "cargo" 접근 방식으로 이것을 핸들링 할 수 있지만 아래 예제를 통해 알 수 있듯이 많은 값들을 전달하기 시작해야 한다는 것을 알게 될 것이다. message-passing의 OOP 스타일의 접근방식은 흥미롭다. (Smalltalk 또는 Self 가 이것과 비슷하다. 또한 여러 OOP xBase variants 도 그렇다).:


Listing 8. Smalltalk스타일(Python)의 tax 계산

class TaxCalc:
    def taxdue(self):
        return (self.income-self.deduct)*self.rate
    def setIncome(self,income):
        self.income = income
        return self
    def setDeduct(self,deduct):
        self.deduct = deduct
        return self
    def setRate(self,rate):
        self.rate = rate
        return self
print "Smalltalk-style taxes due =", \
      TaxCalc().setIncome(50000).setRate(0.30).setDeduct(10000).taxdue()

각각의 "setter"와 함께 self 를 리턴하여 "현재의" 의 것을 모든 매소드 애플리케이션의 결과로서 다룰 수 있다. 이는 FP의 closure 접근방식과 유사한 점을 가지고 있다.

Xoltar 툴킷을 이용하여 데이터를 함수로 묶고 다중 closure 가 다른 번들을 포함하도록 하는 기능을 가진 완전한 closures를 구현할 수 있다.:


Listing 9. Python 함수 스타일의 tax 계산

from functional import *

taxdue        = lambda: (income-deduct)*rate
incomeClosure = lambda income,taxdue: closure(taxdue)
deductClosure = lambda deduct,taxdue: closure(taxdue)
rateClosure   = lambda rate,taxdue: closure(taxdue)

taxFP = taxdue
taxFP = incomeClosure(50000,taxFP)
taxFP = rateClosure(0.30,taxFP)
taxFP = deductClosure(10000,taxFP)
print "Functional taxes due =",taxFP()

print "Lisp-style taxes due =", \
      incomeClosure(50000,
          rateClosure(0.30,
              deductClosure(10000, taxdue)))()

우리가 정의해 놓은 각각의 closure 함수는 함수 범위 안에 정의된 모든 값을 가진다. 그리고 그러한 값을 함수 객체의 global 범위 안으로 묶는다. 하지만 함수의 global 범위로 나타난 것은 반드시 실제의 모듈 global 범위와 같은 것은 아니다. 다른 closure 의 "global" 범위와 동일하지 않을 수도 있다. closure 는 "데이터를 옮길(carry) 뿐" 이다.

예제에서 볼 수 있듯이 몇 가지 특별한 함수를 closure 의 범위 (income, deduct, rate)안으로 바인딩하는데 사용했다. 모든 변하기 쉬운 바인딩을 범위 안 으로 놓기 위해 디자인을 변경하는 것은 간단하다. 예제에서 우리는 두개의 다른 함수 스타일을 사용했다. 첫번째는 taxFP가 변할 수 있도록 하여 연속적으로 추가 값들을 closure scope 안으로 바인딩한다. 이렇게 "closure 에 추가되는 (add to closure)" 라인들은 어떤 순서대로 나타날 수 있다. 하지만 tax_with_Income 과 같은 변하지 않는 이름들을 사용했다면 특정한 순 서로 바인딩 라인을 정렬해야 한다. 그리고 전(earlier) 바인딩을 다음 바인딩으로 전달해야 한다.어떤 경우 필요한 모든 것이 closure 범위 안으로 바인드되면 "seeded" 함수를 호출 할 수 있다.

두 번째 스타일은 약간 Lisp 와 비슷한 것처럼 보인다. 두번째 스타일에서 재미있는 두 가지 일이 발생할 수 있다. 그 첫번째는 이름 바인딩이 모두 안된다. 두 번째 스타일은 어떤 스테이트먼트도 사용되지 않은 단일 수식이다.(Part 1 참조).

closure 의 "Lisp-style" 를 사용할 때 또 하나의 재미있는 일은 이것이 "Smalltalk-style" message-passing 매소드와 많이 닮았다는 것이다. 그 두 가지는 taxdue() 함수/메소드를 호출하면서 값을 합한다. "Smalltalk-style" 은 각각의 단계 사이에 object 를 전달한다 (올바른 데이터를 사용할 수 없다면 두 가지 모두 미완성(crude) 버전에서 에러를 일으킨다). 반면 " Lisp-style"은 연속적으로 전달한다. 하지만 좀더 깊이 들어가면 함수 프로그래밍과 객체 지향 프로그래밍은 같은 결론에 도달한다.




위로


맺음말

이 글에서 우리는 함수 프로그래밍에 대해 알아보았다. 농담을 줄인 덕택에 이전 글보다 짧다. 하지만 개념은 이 글에 설명되어 있음을 명심하라. functional 모듈의 소스를 읽는 것이 다양한 FP 개념들을 이해할 수 있는 좋은 방법이다. 그 모듈은 설명이 잘 되어 있으며 함수/클래스의 예제가 있다. 이 글에서 다루어지지 않은 것 이 있다면 그것은 다른 함수들의 조합(combination)과 interaction을 핸들하기 쉽도 록 하는 단순화 된 메타 함수일 것이다. 이것은 Python 프로그래머들이라면 반드시 알아 두어야 할 사항이다.




위로


참고자료




위로


필자소개

David Mertz는 20년 동안 프로그래머와 작가로 활동해 왔다. 하지만 프로그래밍에 대한 글은 최근에 쓰기 시작했다. 실제로 그는 IT에 지대한 관심을 가지고 있는 인문학 교수라는 소개가 정확할 지 모른다. 소프트웨어를 개발하고 개발과 관련하여 집필활동을 하기도 하지만 어떤 잡지에는 정치 철학이라는 다소 현학적이고 모호한 분야에 대한 글도 쓰는 등 다양성을 지닌 인물이다.


* 출처 : 한국 IBM (http://www.ibm.com/developerworks/kr/library/l-prog2.html)