본문 바로가기

Job Notes/Programming

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

Charming Python: Python에서의 프로그래밍, Part 1

Python을 효과적으로 활용하기

 

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


난이도 : 초급

David Mertz 박사, 응용철학자, Gnosis Software

2001 년 3 월 01 일

일반적으로, 사용자들은 Python이 절차적(procedural)이며 객체 지향적 언어라고 생각하지만 실제로는 함수 프로그래밍에 필요한 모든 것을 갖추고 있는 언어라고 할 수 있다. 함수 프로그래밍의 개념과 Python에서 함수 기술을 구현하는 방법에 대해 알아보자.

"함수 프로그래밍(FP-Functional Programming)이란 무엇일까요?" 라는 질문에 대해, "FP란 Lisp, Scheme, Haskell, ML, OCAML, CLEAN, Mercury, Erlang와 같은 언어로 프로그램을 만드는 것" 이라는 답이 나올 수 있다. 이것은 "안전"하지만 그다지 명확한 답은 되지 못한다. 무엇보다도 안타까운 사실은 FP가 무엇인지에 대한 명확한 답에 대하여 함수 프로그램을 사용하는 프로그래머들에게 조차도 만족스러운 대답을 얻을 수 없다는 것이다. 코끼리와 장님에 관한 일화가 이 같은 상황에 어울리는 것 같다. FP는 "C, Pascal, C++, Java, Perl, Awk, TCL등의 언어를 이용한 명령형 프로그래밍"에 대비되는 뜻이라는 답도 그럴듯하다.

개인적으로 다음과 같이 함수 프로그래밍의 특징을 정리해 보았다.

  • 함수는 클래스(객체)이다. 다시 말해서, '데이터'를 가지고 할 수 있는 모든 것은 함수들 자체만으로도 수행 될 수 있다. (예, 한 함수를 다른 함수로 넘기기)
  • 기본적인 제어 구조로 '재귀(recursion)'이 사용된다. 어떤 언어에는 "루프(loop)" 구조가 존재하지 않는다.
  • LISt Processing에 초점이 맞춰져 있다(Lisp). 리스트는 루프 대신 하위 리스트에 재귀 되어 사용된다.
  • "순수한" 함수 언어는 부작용이 없다. 프로그램 상태를 조사하기 위해서 같은 변수에 처음 값을 지정하고 난 후 다시 다른 값을 매기는 식의 명령형 언어 특성을 배제시켰다.
  • FP는 문장(statements)을 사용하지 않는다. 대신, 수식(expression)을 검사(evaluation)하는 방식을 사용한다. (즉 인자(argument)를 갖는 함수). 간단한 경우, 한 개의 프로그램은 한 개의 expression이 된다.
  • FP에서는 어떻게(what) 계산되는가 보다는 무엇이(how) 계산되는가가 더욱 중요하다.
  • 대부분의 FP는 "higher order" 함수를 사용한다. (다시 말해서 함수가 그 함수 위에서 작동하고 그 함수는 다시 다른 함수에서 작동하는 구조이다.)

함수 프로그래밍의 지지자들은 이러한 특성들로 인해 개발이 빨라지며 길이가 짧고 버그가 적은 코드를 만들 수 있다고 주장한다. 또한, 컴퓨터 공학, 논리(logic), 수학 분야의 이론가들은 형식적인 특징을 증명하는 데에도 함수 언어와 프로그램이 명령형 보다 쉽다는 것을 증명했다.

Python 함수의 특징적인 기능

Python l.0 이후의 Python에는 앞서 열거된 FP의 특징들이 대부분 포함되어 있다. Python은 혼합된 언어이다. Python의 객체 지향성이라는 특성을 십분 발휘하면 하고자 하는 일을 충분히 수행할 수 있다. Python 2.0에는 재미있는 문법적 장점으로 list comprehension 이 추가되었다. list comprehension을 통해 새로운 기능이 추가되지는 않았지만 기존의 것들을 더욱 멋지게 보이도록 할 수 있다.

Python 에서 FP의 기본 요소는 map(), reduce(), filter()등의 함수와 lambda 라는 연산자이다. Python 1.x 에서 apply()함수는 한 함수의 리스트 리턴 값을 다른 함수에 직접 적용시킨다. Python 2.0은 더욱 향상된 문법을 제공한다. 이러한 몇 개의 함수들과 기본 오퍼레이터를 이용하면 Python에서 어떤 프로그램이라도 충분히 만들 수 있다.구체적으로 flow control statement(if, elif, else, assert, try, except, finally, for, break, continue, while, def)는 FP 함수 및 오퍼레이터를 사용하는 함수 스타일에서만 처리될 수 있다. 프로그램에서 마치 Lisp 처럼 보이는 난해한 모든 flow control 명령을 제거하는 대신에 FP가 함수와 재귀적 방법으로 flow control을 어떻게 표현하고 있는 지 이해할 필요가 있다.




위로


flow control 문장 제거하기

이것을 제거할 때 고려해야 할 것은 Boolean expression에 대한 Python의 "short circuit" evaluation 이다. 각각의 블록이 함수 한 개를 호출하고 arrange 된다고 가정할 때, 다음은 if/ elif/ else 블록(blocks)의 expression version을 보여준다.


Listing 1. "short-circuit" 조건 호출(conditional calls)

# Normal statement-based flow control
if <cond1>:   func1()
elif <cond2>: func2()
else:         func3()

# Equivalent "short circuit" expression
(<cond1> and func1()) or (<cond2> and func2()) or (func3())

# Example "short circuit" expression
>>> x = 3
>>> def pr(s): return s
>>> (x==1 and pr('one')) or (x==2 and pr('two')) or (pr('other'))
'other'
>>> x = 2
>>> (x==1 and pr('one')) or (x==2 and pr('two')) or (pr('other'))
'two'

위의 조건 호출 예제는 expression version은 단지 간단한 재주처럼 보일지도 모른다. 하지만 lambda 오퍼레이터가 expression을 리턴시켜야 한다는 것을 알면 점점 재미있는 상황이 된다. expression이 short-circuiting을 사용하는 조건 블록을 포함하기 때문에 lambda expression은 조건 리턴 값을 표현하는데 있어 일반적인 방식이다. 다음 예제를 보자.


Listing 2. lambda의 short-circuiting

>>> pr = lambda s:s
>>> namenum = lambda x: (x==1 and pr("one")) \
....                  or (x==2 and pr("two")) \
....                  or (pr("other"))
>>> namenum(1)
'one'
>>> namenum(2)
'two'
>>> namenum(3)
'other'




위로


퍼스트 클래스(first class) 객체로서 함수

위 예제에서 함수의 퍼스트 클래스 상태를 보았다. lambda 오퍼레이션으로 함수 객체를 만들 때 일반적인 사실을 알 수 있다. 객체를 "pr" 과 "namenum"에 바인딩(binding)했던것과 동일한 방법으로 23이란 숫자나 "spam" 문자열(string)을 그러한 이름들에 바인딩 시킬 수도 있다. 하지만 어떤 이름을 붙이지 않고 23 이란 숫자를 그대로 사용할 수 있는 것처럼(예를 들어 함수의 인수로 사용할 때처럼) lambda를 이용해서 만든 함수 객체도 어떤 이름에도 바인딩시키지 않고 사용할 수 있다. Python에서 함수라는 것은 프로그래머들이 사용하게 될 값이라고 간단히 말할 수 있다.

주된 일은 퍼스트 클래스 객체들을 FP 함수 built-in (map(), reduce(), filter())으로 전달하는 것이다. 각각의 함수들은 첫번째 argument로서 함수 객체를 받아들인다.

  • map()은 받아들인 함수를 지정된 리스트의 각 아이템에 적용해 결과 리스트를 리턴한다.
  • reduce()는 받아들인 함수를 지정된 리스트의 다음 아이템과 최종 결과가 저장된 내부 어큐뮬레이터에 적용한다. 예를 들어reduce(lambda n,m:n*m, range(1,10))은 10의 계승(10!)을 의미한다. (앞서 수행된 곱셈의 결과, 그 다음 각각의 아이템이 곱해진다)
  • filter()는 받아들인 함수를 지정된 리스트의 각 아이템을 평가하는 데 사용한다. 함수 테스트를 통과한 아이템들의 리스트를 선별하여 리턴한다.

가끔씩 함수 객체를 우리가 직접 만든 함수로 보내지만 보통 그러한 것들은 이미 명시된 built-in 의 조합으로서 가능하다.

위 세 개의 FP built-in 함수들을 조합 함으로서 광범위한 범위의 "flow" 오퍼레이션이 실행된다. (구문을 사용하지 않고도 오직 수식(expression)만으로 가능하다)




위로


Python의 함수 루핑

루프를 교체하는 것은 조건 블록을 교체하는 것 만큼 쉬운 일이다. for 는 곧바로 map ()으로 바꿔 사용할 수 있다. 구문 블록을 싱글 함수 호출(call)로 단순화 시킬 수 있다 :


Listing 3. Python의 함수 'for' 루핑

for e in lst:  func(e)      # statement-based loop
map(func,lst)               # map()-based loop

순차적인 제어에 대한 함수 접근을 하는 데에도 비슷한 기능을 적용할 수 있다. 명령형 프로그래밍은 대부분 "이것을 실행하라, 그런 다음 저것을 실행하고 그리고 다음 것을 실행하라" 하는 식으로 문장이 구성되어 있다. map() 을 사용하면 다음과 같이 단순해진다.


Listing 4. Functional sequential actions in Python

# let's create an execution utility function
do_it = lambda f: f()

# let f1, f2, f3 (etc) be functions that perform actions
map(do_it, [f1,f2,f3])    # map()-based action sequence

이 프로그램의 핵심은 프로그램의 완성을 위해 실행하는 함수의 리스트를 가지고 있는 map () expression이라고 볼 수 있다. 퍼스트 클래스 함수들의 또 한가지 편리한 점은 그 함수들을 리스트(list)에 넣을 수 있다는 것이다.

약간은 복잡하지만 while문 역시 FP 스타일로 변환할 수 있다.


Listing 5. Python의 함수 'while' 루핑

# statement-based while loop
while <cond>:
    <pre-suite>
    if <break_condition>:
        break
    else:
        <suite>

# FP-style recursive while loopp
def while_block():
    <pre-suite>
    if <break_condition>:
        return 1
    else:
        <suite>
    return 0

while_FP = lambda: (<cond> and while_block()) or while_FP()
while_FP()

while을 번역하기 위해서는 단지 수식만이 아닌 그 자체에 구문을 포함하는 while_block () 함수가 필요함을 알 수 있다. 그러나 그 함수 자체를 제거할 수 있다. (템플릿에서 if/else대신 short-circuiting을 사용 것과 같은 이치). 루프 바디는 어떤 변수 값도 변경할 수 없기 때문에 while myvar==7 같은 평범한 테스트에 조건식을 사용하기 어렵다. (globals변수는 while_block ()에서 수정될 수 있다) 유용한 조건을 추가 시키는 한가지 방법은 while_block ()을 좀더 그럴싸한 값으로 리턴시키고 종결 조건으로 리턴되는 것과 비교하는 것이다. 구문을 제거하는 구체적인 예제를 보자.


Listing 6. Python의 함수 'echo' 루프

# imperative version of "echo()"
def echo_IMP():
    while 1:
        x = raw_input("IMP -- ")
        if x == 'quit':
            break
        else
            print x
echo_IMP()

# utility function for "identity with side-effect"
def monadic_print(x):
    print x
    return x

# FP version of "echo()"
echo_FP = lambda: monadic_print(raw_input("FP -- "))=='quit' or echo_FP()
echo_FP()

지금까지 재귀(recursion)-실제로 원하는 곳 어디에서나 통과 될 수 있는 함수 객체-을 사용하여 순수하게 수식만을 사용하여 I/O, 루핑, 조건 문이 포함된 프로그램을 바꿔보았다. 비록 유틸리티 함수 monadic_print()를 사용했지만 이 함수는 일반적인 내용을 가지고 있으며 한 번 만들어두면 나중에 우리가 만들 함수 프로그램 expression에서 다시 사용 될 수 있다. monadic_print(x)를 포함하는 모든 expression이 단순히 x 값만을 evaluation 할 뿐이다. FP (특히 Haskell)는 "아무런 역할을 하지 않고 부작용(side-effect)만을 가지는" 함수를 말하는 "monad"라는 개념이 있다.




위로


부작용 제거하기

"왜?" 구문을 제거하고 대신 모호한 중첩 수식으로 대체하는지 의문이 생길 것이다. FP에 대한 모든 해답은 Python을 통해 설명할 수 있다. 무엇보다도 가장 중요한 이유는 부작용을 없애는 데에 있다. (아니면 적어도 monads 와 같은 특별한 영역을 가지고 있다). 프로그래머들을 디버깅 하는 사람으로 만들어버리는 요인은 변수들이 프로그램의 실행 과정에서 예견하지 못한 값을 가지게 된다는 것이다. 함수 프로그램은 변수들에 값을 전혀 할당하지 않기 때문에 이러한 특별한 문제들을 피해갈 수 있다.

이제는 일반적인 명령형 코드를 살펴보자. 여기서의 목표는 결과가 25이상으로 나오는 숫자 쌍의 리스트를 출력하는 것이다. 쌍을 이루는 숫자들은 두개의 다른 리스트에서 얻을 수 있다. 이러한 종류의 일들은 프로그래머들이 실제로 그들의 프로그램의 세그먼트에서 실행하는 일과 비슷하다. 같은 목표를 위해 명령형적 접근은 어떤 식으로 수행되는지 알아보자.


Listing 7. Imperative Python code for "print big products"

# Nested loop procedural style for finding big products
xs = (1,2,3,4)
ys = (10,15,3,22)
bigmuls = []
# ...more stuff...
for x in xs:
    for y in ys:
        # ...more stuff...
        if x*y > 25:
            bigmuls.append((x,y))
            # ...more stuff...
# ...more stuff...
print bigmuls

이 프로젝트는 전혀 잘못된 일이 없을 것 처럼 보이는 간단한 코드이다. 그러나 실제로는 동시에 다른 많은 목표들을 성취하는 코드 안에 일부분으로서 포함되어질 것이다. "more stuff"라는 코멘트가 있는 부분은 부작용으로 인해서 버그가 생길 수 있는 부분이다. xs, ys, bigmuls, x, y 와 같은 변수들은 가설을 토대로 만든 생략 코드에서 예견되지 못한 값을 가지게 될 것이다. 더욱이 이러한 코드가 실행된 후에는 모든 변수들은 다음 코드가 기대하는 것이든 그렇지 않든 상관없이 어떠한 값을 가지게 될 것이다. 함수/인스턴스의 캡슐화는 이러한 유형의 에러로부터 방어하는 데 사용될 수 있다. 그리고 작업이 끝난 다음에 여러분은 항상 변수들을 del을 사용하여 삭제 할 수 있다. 하지만 실전에서 그와 같은 에러는 일반적으로 잘 발생한다.

함수적 접근에 있어서 우리가 목표하는 것은 이러한 부작용 에러를 모두 제거하는 것이다. 다음은 쓰일 수 있는 코드의 일부분이다.


Listing 8. "print big products"에 대한 함수 Python 코드

bigmuls = lambda xs,ys: filter(lambda (x,y):x*y > 25, combine(xs,ys))
combine = lambda xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs)))
dupelms = lambda lst,n: reduce(lambda s,t:s+t, map(lambda l,n=n: [l]*n, lst))
print bigmuls((1,2,3,4),(10,15,3,22))

익명의 (lambda) 함수 객체를 예제와 같이 이름에 바인딩했지만 필수 사항은 아니다. 가독성(readability)을 위해 이러한 방식을 채택했다. 하지만 combine()는 무엇이든 가질 수 있는 훌륭한 유틸리티 함수이다. (두개의 input 리스트에서 모든 쌍의 엘리먼트 리스트를 만든다). dupelms()는 combine()를 완성시키는 함수이다. 함수 예제가 명령형 예제 보다 장황하다. 하지만, 일단 유틸리티 함수를 다시 사용하면 bigmuls()의 새로운 코드는 명령형 버전에서보다는 훨씬 적을 것이다.

함수 예제의 장점은 확실히 어떤 변수도 그 안의 어떤 값을 바꾸지 않는다는 것이다. 다음의 코드(또는 그 이전의 코드)에 전혀 예견하지 못한 부작용은 없을 것이다. 부작용이 적다는 사실만으로 코드의 정확성이 보장되지는 않는다. 하지만 이것은 훌륭한 장점은 있다.

많은 함수 언어들과는 다르게 Python은 bigmuls, combine, dupelms 라는 이름들에 다른 것이 다시 바인딩 되는 것을 막지 못한다. 만일 combine ()이 다음 프로그램에서 다른 것을 의미한다면 모든 생각은 빗나간 것이다. Singleton 클래스를 발전시켜서 s.bigmuls과 같은 유형의 변경되지 않는 바인딩(binding)을 포함시켜도 된다. 하지만 이 글에서는 다루지 않겠다.

우리의 목표는 Python 2 기능에 맞춘 프로그램을 만드는 것이다. 주어진 예제가 명령이든 함수이든 다음은 최상의 함수 테크닉이 될 것이다.


Listing 9. "bigmul"를 위한 리스트 포함 Python 코드

print [(x,y) for x in (1,2,3,4) for y in (10,15,3,22) if x*y > 25]




위로


결론

지금까지 프로세스 상에 부작용을 없애면서 모든 Python flow-control 구조에 대해 함수로 교체시키는 방법을 설명했다. 특별한 프로그램을 효율적으로 변환하기위해서는 몇 가지 더 고려해야 겠지만 함수 built-in이 일반적이고 완전하다. 다음 칼럼에서는 함수 프로그래밍에 쓰일 수 있는 좀더 향상된 기술을 설명하겠다. 또한 함수 프로그래밍의 장점과 단점에 대해서 더 알아볼 수 있을 것이다.




위로


참고자료

  • developerWorks worldwide 사이트에서 이 기사에 관한 영어원문.

  • functional 모듈이 포함되어 있는 Bryn Keller의 "xoltar toolkit", 에는 Python에 유용한 확장 FP가 제공된다. functional모듈이 전적으로 Python으로 작성되었기 때문에 이미 Python에서는 사용이 가능하다. 하지만 Keller는 이 툴킷을 훌륭하게 통합된 확장형 세트로 고안했다.

  • Peter Norvig이 쓴 기사 중에 Lisp 프로그래머를 위한 Python. 글의 초점이 다르지만 그런 만큼 Python과 Lisp를 객관적으로 비교할 수 있을 것이다.

  • 함수형 프로그래밍을 처음 시작하는 경우라면 comp.lang.functional의 FAQ을 이용할 것..

  • Lisp/Scheme 이 Emacs에서 만큼은 독보적 위치지만, 함수 프로그래밍을 이해하기 위해서라면 Lisp/Scheme 보다 Haskell 언어가 적격이라고 생각한다. 다른 Python 프로그래머는 많은 괄호와 접두 연산자 없이도 쉽게 같은 작업을 할 수 있다.

  • Simon Thomson (Addison-Weseley, 1999):Haskell: The Craft of Functional Programming (2nd Edition)



위로


필자소개

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

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