본문 바로가기

Job Notes/Programming

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

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

Currying과 기타 higher-order 함수들


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


난이도 : 초급

David Mertz 박사Gnosis Software, Inc

2001 년 6 월 01 일

David Mertz 박사는"Python에서의 함수 프로그래밍", Part 1 & Part 2 시리즈를 통해 함수 프로그래밍의 기본 개념을 다루었다. 이번에는 Xoltar Toolkit에 있는 커링(currying)과 기타 higher-order 함수에 대해 설명한다.

정규식 바인딩

부분적 솔루션에 절대 만족하지 말라! Richard Davies라는 한 독자는 바인딩을 개별 정규식으로 항상 옮길 수 있는지 여부에 대해 의견을 제시했다. 그렇게 하는 것이 왜 필요한 것인지 이유와 comp.lang.python 기여자가 제공한 정규식 방법을 설명하겠다.

우선, functional 모듈에서 Bindings클래스를 호출하자. 이 클래스의 속성을 이용하여 특별한 이름이 주어진 블록 스코프안에서 단지 하나를 의미한다는 것을 확인할 수 있다:


Listing 1: Python FP session with guarded rebinding


>>> 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.
>>> let.car(range(10))
0

Bindings 클래스는 모듈 또는 def 함수 스코프 안에서 우리가 원하는 것을 수행한다. 그러나 단일 정규식 내에서 작동하게 하는 방법은 없다. ML 계열 언어에서는, 단일 정규식 내에서 바인딩을 만드는 것은 자연스러운 일이다:


Listing 2: Haskell expression-level name bindings

-- car (x:xs) = x  -- *could* create module-level binding
list_of_list = [[1,2,3],[4,5,6],[7,8,9]]

-- 'where' clause for expression-level binding
firsts1 = [car x | x <- list_of_list] where car (x:xs) = x

-- 'let' clause for expression-level binding
firsts2 = let car (x:xs) = x in [car x | x <- list_of_list]

-- more idiomatic higher-order 'map' technique
firsts3 = map car list_of_list where car (x:xs) = x

-- Result: firsts1 == firsts2 == firsts3 == [1,4,7]

Greg Ewing는 Python의 list comprehension을 사용하여 같은 효과를 얻을 수 있다는 것을 발견했다; Haskell의 신택스처럼 깨끗한 방식으로 가능하다:


Listing 3: Python 2.0+ expression-level name bindings

>>> list_of_list = [[1,2,3],[4,5,6],[7,8,9]]
>>> [car_x for x in list_of_list for car_x in
 (x[0],)]
[1, 4, 7]

단일 아이템 튜플안에서 정규식을 list comprehension에 넣는 트릭은 higher-order 함수를 이용한 정규식 레벨 바인딩 방식이 아니다. higher-order 함수를 사용하기 위해서는, 다음과 같이 블록 레벨 바인딩을 사용해야 한다:


Listing 4: Python block-level bindings with 'map()'

>>> list_of_list = [[1,2,3],[4,5,6],[7,8,9]]
>>> let = Bindings()
>>> let.car = lambda l: l[0]
>>> map(let.car,list_of_list)
[1, 4, 7]

map()을 사용하고 싶다면 바인딩 범위는 우리가 원한 것 보다 약간 더 넓어지게 된다. 하지만 list comprehension으로 네임 바인딩을 수행할 수 있다.


Listing 5: "Stepping down" from Python list comprehension

# Compare Haskell expression:
# result = func car_car
#          where
#              car (x:xs) = x
#              car_car = car (car list_of_list)
#              func x = x + x^2
>>> [func for x in list_of_list
...       for car in (x[0],)
...       for func in (car+car**2,)][0]
2

첫번째 엘리먼트인 list_of_list 에 대해 산술 계산을 수행했다. (정규식 범위에서만). "optimization" 처럼, 시작에 사용 될 엘리먼트 보다 긴 list를 만들지 않다도 된다. 왜냐하면 마지막 index 0과 함께 첫번째 엘리먼트만 선택했기 때문이다:


Listing 6: Efficient stepping down from list comprehension


>>> [func for x in list_of_list[:1]
...       for car in (x[0],)
...       for func in (car+car**2,)][0]
2




위로


Higher-order 함수: currying

Python 에는 가장 일반적인 higher-order 함수 중 세 함수가 내장되어 있다: map(), reduce(), ilter(). 이 함수들이 하는 일은 다른 함수들을 그들의 인자로 취한다는 점이다. 내장되어있는 함수들 이외의 다른 higher-order 함수들은 함수 객체들을 리턴한다.

Python의 고급 함수 객체로 인해서 사용자들은 자신만의 higher-order 함수를 구현할 수 있다. 다음 예제를 보자:


Listing 7: Trivial Python function factory

>>> def foo_factory():
...    def foo():
...        print 
"Foo function from factory"
...    return foo
...
>>> f = foo_factory()
>>> f()
Foo function from factory

Part 2 에서 언급했던 Xoltar Toolkit에는 higher-order 함수 컬렉션이 있다. Xoltar의 함수의 functional 모듈이 제공하는 것중 대부분이 전통적인 함수 언어에서 개발된것이다. 오랜 시간동안, 유용성이 입증되었다.

가장 유명하고, 가장 중요한 higher-order 함수는 curry()일 것이다. curry()는 논리학자 Haskell Curry의 성을 따라 지었다. 그의 이름 또한 프로그래밍 언어의 이름이다. "currying"은 모든 함수를 단지 한 인자의 부분적 함수로 취급할 수 있다는 점이 가장 놀랍다. currying 이 작동하기 위해서 필요한 것은 함수의 리턴 값이 함수가 될 수 있도록 하는 것이다. 하지만 "narrowed" 또는 "closer to completion." 과 같은 리턴 된 함수를 이용해야 한다. Part 2에서 썼던 closures와 매우 유사하게 작동한다-- 커리 된 리턴 함수인 "fills in"으로의 모든 연속된 호출은 최종 계산에 포함된다.(프로시져에 첨부된 데이터).

Haskell에서 간단한 예제를 통해 currying을 설명하겠다. 마찬가지로 Python 에서도 functional 모듈을 사용해 보겠다. 예제를 보자:


Listing 8: Currying a Haskell computation

computation a b c d = (a + b^2+ c^3 + d^4)
check = 1 + 2^2 + 3^3 + 5^4

fillOne   = computation 1 -- specify "a"
fillTwo   = fillOne 2     -- specify "b"
fillThree = fillTwo 3     -- specify "c"
answer    = fillThree 5   -- specify "d"
-- Result: check == answer == 657

Python:


Listing 9: Currying a Python computation

>>> from functional import curry
>>> computation = lambda a,b,c,d: (a + b**2 + c**3 + d**4)
>>> computation(1,2,3,5)
657
>>> fillZero  = curry(computation)
>>> fillOne   = fillZero(1)  # specify "a"
>>> fillTwo   = fillOne(2)   # specify "b"
>>> fillThree = fillTwo(3)   # specify "c"
>>> answer    = fillThree(5) # specify "d"
>>> answer
657

Part 2 에서 사용되었던 세금 계산 프로그램을 보면 closures에 해당하는 것을 알 수 있다. (이번에는 curry()를 사용할 것임):


Listing 10: Python curried tax calculations

from functional import *

taxcalc = lambda income,rate,deduct: (income-(deduct))*rate

taxCurry = curry(taxcalc)
taxCurry = taxCurry(50000)
taxCurry = taxCurry(0.30)
taxCurry = taxCurry(10000)
print "Curried taxes due =",taxCurry

print "Curried expression taxes due =", \
      curry(taxcalc)(50000)(0.30)(10000)

closures 와는 다르게, 특정 순서(왼쪽에서 오른쪽)로 인수를 커리해야 한다. 단, functional 또한 다른 끝에서 시작 할(오른쪽에서 왼쪽) rcurry() 클래스를 포함하고 있다는 것에 주의하라.

두번째 print 문장은 taxcalc(50000,0.30,10000) 호출에서 단순히 스펠링 변화를 준 것이다. 하지만 다른 레벨에서는 모든 함수가 단지 한 인자의 함수가 될 수 있다는 개념을 다시한번 확인하는 결과가 되었다.




위로


higher-order 함수의 다양성

currying의 "기본적인" 작동 이외에도, functional은 higher-order 함수들을 갖고 있다. 더욱이, higher-order 함수들을 작성하는 것은 어려운 일이 아니다. functional이 있든 없든 상관이 없다.

대부분의 경우, higher-order 함수들은 표준 map(), filter(), reduce()의 "향상된" 버전처럼 보인다. 종종, 이러한 함수들의 패턴은 " 함수(들)과 몇가지 list를 인자로 취하고 그런 다음 함수를 list 인자에 붙인다." 또 다른 패턴은 "함수 모음을 가져다가 그 함수 기능들을 포함하는 함수를 만드는 것이다." 많은 변수들 또한 가능하다. functional이 어떤 것을 제공하고 있는지 살펴보자.

sequential()also() 함수는 컴포넌트 함수들의 결과에 기초해서 함수를 만든다. 컴포넌트 함수들은 같은 인자(들)과 함께 호출될 수 있다. 둘의 큰 차이점은, sequential()이 인자로서 싱글 list를 요구하는 반면 also()는 인자들의 리스트를 취한다. 대부분의 경우, 함수 부작용에 유용하게 쓰일 수 있지만 sequential()은 조합 된 리턴 값을 제공 할 함수를 선택하도록 한다:


Listing 11: Sequential calls to functions (with same args)

>>> def a(x):
...     print x,
...     return "a"
...
>>> def b(x):
...     print x*2,
...     return "b"
...
>>> def c(x):
...     print x*3,
...     return "c"
...
>>> r = also(a,b,c)
>>> r
<functional.sequential instance at 0xb86ac>
>>> r(5)
5 10 15
'a'
>>> sequential([a,b,c],main=c)('x')
x xx xxx
'c'

이전에는 같은 인자(들)을 이용하여 함수로 다중 함수들을 호출했던 상태에서 이제는, any(), all(), none_of() 로 인자들의 리스트에 대해서 같은 함수를 호출하게 한다. 일반적 구조에서 이러한 것들은 어느 정도 내장된 map(), reduce(), filter() 함수들과 비슷하다. 하지만 higher-order 함수들은 리턴 값의 모음에 대해서 Boolean 질문을 한다. 예를 들어:


Listing 12: Ask about collections of return values

>>> from functional import *
>>> isEven = lambda n: (n%2 == 0)
>>> any([1,3,5,8], isEven)
1
>>> any([1,3,5,7], isEven)
0
>>> none_of([1,3,5,7], isEven)
1
>>> all([2,4,6,8], isEven)
1
>>> all([2,4,6,7], isEven)
0

특히 매력적인 higher-order 함수는 compose()이다. 다양한 함수들의 조합은 한 함수의 리턴 값을 다음 함수의 인풋에 "함께 묶는 것" 이다. 다양한 함수들을 조합하는 프로그래머들은 아웃풋과 인풋이 맞도록 해야한다. 그리고 프로그래머가 리턴 값을 사용할 때 마다 언제나 true 이어야 한다. 다음 예제를 보면 좀더 이해가 될 것이다:


Listing 13: Creating compositional functions

>>> def minus7(n): return n-7
...
>>> def times3(n): return n*3
...
>>> minus7(10)
3
>>> minustimes = compose(times3,minus7)
>>> minustimes(10)
9
>>> times3(minus7(10))
9
>>> timesminus = compose(minus7,times3)
>>> timesminus(10)
23
>>> minus7(times3(10))
23




위로


맺음말

higher-order 함수에 대해 흥미를 가졌을 것이라 생각한다. higher-order 함수를 한번 만들어 보라. 그 중 어떤 것은 반드시 유용하고 강력하게 작용할 것이다. 나에게 결과를 알려주기 바란다.




위로


참고자료




위로


필자소개

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


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