DNA 포럼 API 서비스 모음 DNA Lens

Funtional Programming in Python


  • 전창경(이마케팅본부 e-M솔루션팀), 2006년 10월

초록(Abstract)

함수형 프로그래밍이 무엇인지를 알아보고 파이썬으로 함수형 프로그래밍을 하는 방법에 대해서 알아보자.

함수형 프로그래밍이란 #

함수형 프로그래밍(Functional Programming)이란 입력과 출력이 있는 함수를 기반으로 프로그래밍을 하는것으로 여기서 함수라함은 내부상태가 존재하지 않아서 항상 같은 입력에는 같은 출력이 나오는 방식으로 작동하는 것을 의미한다. 순수 함수형 프로그래밍에서는 어떠한 변수의 사용도 허용하지 않는다. 변수가 없을때의 장점은 부작용이 없다는것이다. 부작용이란 내가 변경한 어떠한 구문이 현재의 위치외의 다른곳에 영향을 미치는것을 의미한다. 이러한 영향을 미치기 위해서는 그 매개체인 변수가 존재하여야 하고 변수가 존재하지 않는다는 것은 부작용이 없다는것을 의미한다.

함수형 프로그래밍의 특징 #

  • Modularity
    - 함수형 프로그래밍을 하기위해서는 필연적으로 모든 기능들을 더 작은 함수들로 나누어야 한다.
    그렇게 하지않고는 실질적으로 프로그래밍이 거의 불가능하다. 이렇게 작게 나눔으로서 나중의 유지보수와 분석도 수월해지게 된다.
  • Ease of debugging and testing
    - 각각의 함수는 명확하게 자신의 역할이 정해지고 작은단위로 나뉘어지기 때문에 테스트및 디버깅이 쉽고
    더군다나 외부의 상황등에 전혀 영향을 받지 않기 때문에 더욱 테스트가 쉬워진다.

파이썬에서의 함수형 프로그래밍 #


Iterator #

  • 순차적이고 연속적인 정보를 표현하기 위한 구조체로 항상 next() 라는 내부함수를 이용해서 다음 원소를 참조한다.
    >>> L = [1,2]
    >>> it = iter(L)
    >>> print it
    <iterator object at 0x8116870>
    >>> it.next()
    1
    >>> it.next()
    2
    >>> it.next()
    Traceback (most recent call last):
      File "<stdin>", line 1, in ?
    StopIteration
    
  • for X in Y 에서 Y는 iterator이거나 iterable이어야 한다. 그러므로 다음 두개는 똑같다.
    for i in iter(obj):
        print i
    
    for i in obj:
        print i
    
  • 아래와 같이 사용 가능하다.
    >>> L = [1,2,3]
    >>> iterator = iter(L)
    >>> t = tuple(iterator)
    >>> t
    (1, 2, 3)
    >>> a,b,c = iterator
    >>> a,b,c
    (1, 2, 3)
    


higher-order function #

  • 함수자체를 입력과 출력에 사용할수 있는 함수를 의미한다.

Bulit-in Functions #

  • map
    >>> def cube(x): return x*x*x
    >>> map(cube, range(1, 11))
    [1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]
    
  • filter
    >>> def f(x): return x % 2 != 0 and x % 3 != 0
    >>> filter(f, range(2, 25))
    [5, 7, 11, 13, 17, 19, 23]
    
  • reduce
    리스트의 처음 두개의 원소에 함수를 적용하고 그 결과와 그 다음 원소들에 계속적으로 함수를 적용하여 최종으로 하나의 결과를 만들어낸다.
    reduce
    >>> def add(x,y): return x+y
    >>> reduce(add, range(1, 11))
    55           //   ((((1+2)+3)+4)+5)...
    


lambda Function #

  • 이름없는 작은함수를 의미한다.
  • 수학적으로 증명된 바에 의하면 모든것은 함수로 표현될수 있다고 한다.
    >>> P=pair(2,3)
    >>> first(P)
    2
    >>> second(P)
    3
    
    위의 예는 단순하게 보기에는 OOP적으로 class를 만든것처럼 보인다. 하지만 이것도 lambda를 이용해서 표현해 낼수 있다.
    >>> true = lambda x, y: x
    >>> false = lambda x, y: y
    
    >>> pair = lambda x, y: lambda f: f(x, y)
    >>> first = lambda p: p(true)
    >>> second = lambda p: p(false)
    
    여기서 보이는 pair라는 함수는 일종의 currying을 이용한 함수이고(currying은 다음항목을 참고) 내부적으로 변수만 할당된 lambda함수에 first/second에서는 실제 적용할 함수를 넘겨주게 된다.

currying #

  • 함수의 출력으로 새로운 함수를 내보내는 기능을 의미한다.
  • 위의 예처럼 lambda를 이용해서 간단히 구현가능하다.
    >>> def mul(x):
    ...   return lambda(y): x*y
    ...
    >>> double = mul(2)
    >>> double(2)
    4
    >>> mul(2)(5)
    10
    
  • 그 외에도 파이썬에서는 currying을 위한 자체 기능도 지원한다.
    double = curry(operator.mul,2)
    
    class curry:
        def __init__(self, fun, *args, **kwargs):
            self.fun = fun
            self.pending = args[:]
            self.kwargs = kwargs.copy()
        def __call__(self, *args, **kwargs):
            if kwargs and self.kwargs:
                kw = self.kwargs.copy()
                kw.update(kwargs)
            else:
                kw = kwargs or self.kwargs
            return self.fun(*(self.pending + args), **kw)
    

functool module #

  • higher-order function들을 구현한 2.5에 추가된 module
  • functool.partial() - 지정된 함수와 지정된 파라미터를 추가하여 새로운 함수를 만들어낼 수 있다.
    import functools
    def log (message, subsystem):
        "Write the contents of 'message' to the specified subsystem."
        print '%s: %s' % (subsystem, message)
        ...
    server_log = functools.partial(log, subsystem='server')  
          // log(subsystem='server' 에 다른 parameter를 추가로 받을수 있다.
    server_log('Unable to open socket')
    


lazy evaluation #

minimal evaluation #

  • lazy evaluation의 한가지로 항상 최소한의 evaluation만을 수행하는 기능을 의미한다.
  • x or y 의 경우에 x가 거짓이면 y와 상관없이 무조건 거짓이다. 그러므로 x가 참일경우에만 y를 수행하도록 되어있다.
  • x and y 의 경우는 반대로 x가 참이면 무조건 참이된다. 그러므로 x가 참인 경우에는 y는 수행할 필요가 없어진다.
  • 그래서 아래와 같은 구문이 가능해진다.
    a = x ? x1 : x2;         // C의 구문
    --> a = x and x1 or x2   // x가 참이면 x1을 수행하고 거짓이면 x2를 수행한다.
    

lazy evaluation - itertool module #

  • lazy evaluation은 evaluation을 꼭 필요한 만큼만 수행하는것을 의미하며 itertool module을 통해서 지원된다.
  • 그래서 무한개의 숫자로 된 리스트에 대한 표현이 가능해진다.
    itertools.count()                            => 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
    itertools.cycle([1,2,3,4,5])                 => 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
    itertools.repeat('abc')                      => abc, abc, abc, abc, abc, abc, ...
    itertools.repeat('abc', 5)                   => abc, abc, abc, abc, abc
    itertools.chain(['a', 'b', 'c'], (1, 2, 3))  => a, b, c, 1, 2, 3
    itertools.izip(['a', 'b', 'c'], (1, 2, 3))   => ('a', 1), ('b', 2), ('c', 3)
    itertools.islice(itertools.count(), 8)       => 0, 1, 2, 3, 4, 5, 6, 7
    itertools.islice(itertools.count(), 2, 8)    => 2, 3, 4, 5, 6, 7
    itertools.islice(itertools.count(), 2, 8, 2) => 2, 4, 6   
                                   // 3번째부터 8번째까지 2번간격으로 꺼내오기
    itertools.tee( itertools.count() )           => iterA, iterB        
                                   // 똑같은 iter를 두개 return한다.
                                     where iterA -> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
                                     and   iterB -> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
    itertools.imap(operator.add, [5, 6, 5], [1, 2, 3]) =>  6, 8, 8
                                   // 원소 각각을 페어로 만들어 함수를 적용한다.
    itertools.starmap(os.path.join,
                      [('/usr', 'bin', 'java'), ('/bin', 'python'),
                       ('/usr', 'bin', 'perl'),('/usr', 'bin', 'ruby')])
                                   // 원소 각각에 대해서 함수를 적용한다.
              => /usr/bin/java, /bin/python, /usr/bin/perl, /usr/bin/ruby
    itertools.ifilter     (lambda x: (x%2)==0, itertools.count())  => 0, 2, 4, 6, 8 ...
    itertools.ifilterfalse(lambda x: (x%2)==0, itertools.count())  => 1, 3, 5, 7, 9 ...
    itertools.takewhile(lambda x:(x<8), itertools.count()) => 0, 1, 2, 3, 4, 5, 6, 7
    itertools.takewhile(lambda x: (x%2)==0, itertools.count()) => 0
                                   // 거짓이 되기 바로전까지 꺼내오기
    itertools.dropwhile(lambda x:(x<10), itertools.count())    => 10, 11, 12, 13, 14 ...
    itertools.dropwhile(lambda x: (x%2)==0, itertools.count()) => 1, 2, 3, 4, 5, 6 ...
                                   // 거짓이 되기 바로전까지를 제외하고 나머지만
    
    마지막으로 groupby() 는 지정된 함수를 적용하여 나온값에 의해서 그룹핑된 새로운 iter들을 리턴하는 함수이다.
    city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
                 ('Anchorage', 'AK'), ('Nome', 'AK'),
                 ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
                 ...
                ]
    
    def get_state ((city, state)):
        return state
    
    itertools.groupby(city_list, get_state) =>
      ('AL', iterator-1),
      ('AK', iterator-2),
      ('AZ', iterator-3), ...
    
    where
    iterator-1 =>
      ('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
    iterator-2 =>
      ('Anchorage', 'AK'), ('Nome', 'AK')
    iterator-3 =>
      ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')
    


Generator expressions and list comprehensions #

  • 파이썬은 모든 원소에 특정 작업을 수행하거나, 특정조건에 맞는 원소만을 뽑아내거나 하는 등의
    아주 흔한 기능을 수행하기 위한 편한 방법을 제공한다. iterator나 list로 만들수 있다.
    line_list = ['  line 1\n', 'line 2  \n', ...]
    # Generator expression -- returns iterator
    stripped_iter = (line.strip() for line in line_list)
    # List comprehension -- returns list
    stripped_list = [line.strip() for line in line_list]
    


Generator expressions #

  • Generator는 다음과 같은 구문을 갖는다.
    ( expression for expr in sequence1
                 if condition1
                 for expr2 in sequence2
                 if condition2
                 for expr3 in sequence3 ...
                 if condition3
                 for exprN in sequenceN
                 if conditionN )
    
    위의 구문은 다음과 같은 역할을 한다.
    for expr1 in sequence1:
        if not (condition1):
            continue   # Skip this element
        for expr2 in sequence2:
            if not (condition2):
                continue    # Skip this element
            ...
            for exprN in sequenceN:
                 if not (conditionN):
                     continue   # Skip this element
    
                 # Output the value of
                 # the expression.
    


List Comprehensions #

  • filter,map등을 이용해서 처리하는 기능을 좀더 직관적으로 표현할 수 있는 방법을 제공한다.
    >>> vec = [2, 4, 6]
    >>> map ( lambda x: 3*x , vec )
    >>> [3*x for x in vec]
    >>> map (lambda x:3*x, filter(lambda x: x>3 , vec))
    >>> [3*x for x in vec if x > 3]
    >>> filter(lambda x: x<15, map( lambda x:3*x, vec) )
    >>> [x for x in [3*x for x in vec] if x<15]
    
  • 추가적으로 두개이상의 리스트에 대해서 * 방식으로 계산도 가능하다.
    >>> vec1 = [2, 4, 6]
    >>> vec2 = [4, 3, -9]
    >>> [x+y for x in vec1 for y in vec2]
    [6, 5, -7, 8, 7, -5, 10, 9, -3]
    >>> [vec1[i]*vec2[i] for i in range(len(vec1))]
    [8, 12, -54]
    


Generator #

  • Iterator를 생성하기 위한 기능으로 간단한 예제는 다음과 같다.
    def generate_ints(N):
        for i in range(N):
            yield i
    
    >>> gen = generate_ints(3)
    >>> gen
    <generator object at 0x8117f90>
    >>> gen.next()
    0
    >>> gen.next()
    1
    >>> gen.next()
    2
    >>> gen.next()
    Traceback (most recent call last):
      File "stdin", line 1, in ?
      File "stdin", line 2, in generate_ints
    StopIteration
    
  • 파이썬 2.5부터는 next()대신 send()를 통해서 특정 값을 전달해 줄수 있게되었다.
    def counter (maximum):
        i = 0
        while i < maximum:
            val = (yield i)
            # If value provided, change counter
            if val is not None:
                i = val
            else:
                i += 1
                
    >>> it = counter(10)
    >>> print it.next()
    0
    >>> print it.next()
    1
    >>> print it.send(8)
    8
    >>> print it.next()
    9
    >>> print it.next()
    Traceback (most recent call last):
      File ``t.py'', line 15, in ?
        print it.next()
    StopIteration
    
  • coroutines이란 subroutines의 일반적인 형태로 하나의 함수가 수행중에 잠시 중단되면서 결과를 리턴하고
    나중에 다시 계속 진행될수 있는 구조를 의미한다. generator의 yield와 next가 정확히 이와 같은 역할을 하는 기능이다.
  • Generator를 이용하면 coroutines를 파이썬에서 구현이 가능해진다.