Python으로 보는 함수형 프로그래밍

Python은 특정 프로그래밍 방식을 강제하지 않기 때문에 절차 지향, 객체지향, 함수형 등 다양한 방식으로 코드를 이어나갈 수 있다. 함수형 프로그래밍은 코드의 가독성을 높이고, 디버깅을 용이하게 만드는 등 여러 장점을 가지는 방식이다. Python에서도 map이나 filter와 같은 유용한 함수를 통해 그러한 장점을 살릴 수 있다. 

하지만 함수형 프로그래밍을 Python에 적용하는 것이 항상 옳다고는 말할 수 없다. 파이썬 공식문서 중 "Functional Programming HOWTO"를 읽어봐도 functools.reduce와 같은 함수의 사용보다 for 루프를 사용한 명시적 표현이 더 명확할 수 있다고 언급하였다. 그렇기 때문에 특정 방식에 집착하기보다 코드의 가독성과 명시성이 중요하다는 대원칙을 고려해 상황에 따라 효율적이고 적절한 방법을 선택하는 것이 중요하다고 생각한다.

Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.

- The Zen of Python, by Tim Peters

순수함수

순수 함수는 부수효과(side effect)가 없는 함수로 정의할 수 있다. 

부수효과가 없다는 것은 변수의 값이나 상태를 변경되지 않고, 예외나 오류가 발생하지 않아야 한다는 의미이다. 즉, 순수 함수는 외부로부터 독립적이고 함수 외부에 영향을 주지 않는다. 또한 특정 입력에 대해서 항상 같은 출력을 반환한다. 

# 순수 함수
def minus_two(n):
    return n - 2

num = 5
res = minus_two(num)
print(f"num: {num}, res: {res}")  # num: 5, res: 3

minus_two라는 함수는 외부에 존재하는 "num" 변수에 영향을 미치지 않고 동작한다. 또한 함수에 5라는 상수가 입력되었을 때 항상 3을 반환한다. 

순수 함수가 아닌 경우는 아래와 같다. 

def minus():
    global num
    num = num - const

const = 2
num = 5
minus()
print(num)  # 3

위 함수는 함수가 호출되면서 num의 값이 변경된다. 또한 const 변수의 값의 영향을 받는다. const가 2일 때는 5를 반환하지만 const가 4라면 1을 반환하게 된다. 

Python의 관점에서 생각해보면 순수 함수의 개념을 이용함으로써 코드의 오류 발생 가능성을 줄이고, 항상 같은 결과를 반환하는 안전성 높은 코드를 작성할 수 있다. 더 나아가 함수를 작은 단위로 쪼개어 선언함으로써 코드의 반복을 줄이고 유지보수에 용이한 코드를 작성할 수 있다. 


currying & functools

일급 객체(First Class Citizen)는 변수에 할당할 수 있고, 객체의 인자로 넘길 수 있고, 객체의 반환값으로 사용될 수 있는 객체를 의미한다. 함수형 프로그래밍에서는 함수를 일급 객체로 본다. Python에서도 함수를 일급 객체로 활용할 수 있다. 

간단한 예시를 살펴보면 함수가 아래와 같이 활용될 수 있다.

def get_one():
    return 1

func = get_one
print(func())  # 1

그뿐 아니라 함수의 파라미터로 넘겨 사용할 수도 있다.

def get_one():
    return 1

def add_three(func):
    return func() + 3

res = add_three(get_one)
print(res)

조금 더 복잡하게 아래와 같은 예시도 가능하다.

def add(num1):
    def add_num(num2):
        return num1 + num2
    return add_num

res = add(6)(3)
print(res)  # 9

add_two = add(2)
print(
    add_two(5), 
    add_two(6)
)  # 7 8

add_three = add(3)
print(
    add_three(2), 
    add_three(7)
)  # 5 10

위와 같이 함수 안에 함수를 정의하여 반복적인 호출문을 사용하는 기법을 커링이라고 한다. 커링의 장점은 함수 파라미터를 재활용할 수 있다는 점이다. 하지만 함수를 정의할 때 함수 내에 다른 함수를 정의하기 때문에 코드가 직관적이지 못하다는 단점도 있다. 

이러한 기능을 도와주는 함수가 functools.partial이다. 

import functools

functools.partial(함수, arg1, arg2 ... kwarg1, kwarg2 ... ) -> 일부 인자가 적용된 함수

아래 예시를 보면 일부 인자를 적용하지 않은 상태에서 변수에 함수를 저장할 수 있다. 

import functools

def add_num(num1, num2, num3):
    return num1 + num2 + num3

func = functools.partial(add_num, num2=3)
res = func(num1=5, num3=9)
print(res)  # 17

List Comprehension  & Generator Expression

리스트 컴프리헨션(List Comprehension)과 제너레이터 표현식(Generator Expression)을 활용하면 for 문을 통해 작성하는 것보다 간결하게 리스트나 제너레이터를 생성할 수 있다. 

# List Comprehension
# [ 표현식 반복문 조건문 ] --> 조건문 생략 가능
>>> lst = [i + 3 for i in range(5) if i < 3]
>>> type(lst))
<class 'list'>

# Generator Expression
# ( 표현식 반복문 조건문 ) --> 조건문 생략 가능
>>> gen = (i + 3 for i in range(5) if i < 3)
>>> type(gen)
<class 'generator'>

참고로 이터레이터는 iter() 함수를 통해 선언할 수 있으며, 이터레이터는 list()나 tuple()를 통해 리스트나 튜플 자료형이 될 수 있다. 

>>> a = iter([1, 2, 3, 4])
>>> type(a)
<class 'list_iterator'>
>>> next(a)
1
>>> next(a)
2
>>> a = list(a)
>>> type(a)
<class 'list'>

map

map(반환값이 있는 함수, 시퀸스) -> map 객체

반환값이 있는 함수와 시퀸스를 대입하면 시퀸스 요소들에 함수를 적용한 map 객체를 반환한다. map 객체는 이터레이터와 같이 작동한다. 

def increment(x):
    return x + 1

seq = [0, 1, 2]

# map을 사용한 경우
iterator = map(increment, seq)
res = list(iterator)  # [1, 2, 3]

# for을 사용한 경우
res = list()
for i in seq:
    res.append(increment(i))
# [1, 2, 3]

filter

filter(boolean을 반환하는 함수, 시퀸스) -> filter 객체

True 또는 False를 반환하는 함수를 받아 함수의 반환값이 True인 요소만을 filter 객체로 반환한다. filter 객체는 이터레이터와 같이 작동한다. 

def is_even(x):
    return x % 2 == 0

seq = [1, 2, 3, 4]

# filter를 사용한 경우
iterator = filter(is_even, seq)
res = list(iterator)  # [2, 4]

# for를 사용한 경우
res = list()
for i in seq:
    if is_even(i):
        res.append(i)
# [2, 4]

map이나 filter 외에도

  • enumerate(iterable, start=0): 인덱스와 요소를 튜플로 묶어 반환하는 이터레이터를 반환
  • zip(iterable1, iterable2 ... ): 여러 시퀸스의 같은 인덱스 요소들을 튜플로 묶어 반환하는 이터레이터를 반환
  • any(iterable): 시퀸스 내 어떤 요소가 참이면 True 반환
  • all(iterable): 시퀸스 내 모든 요소가 참이면 True 반환

등 여러 내장 함수를 지원한다. 


itertools

itertools 모듈은 이터레이터를 활용하기 위한 함수를 제공한다. 

import itertools

itertools.count(start=0, step=1) # 무한한 스트림을 반복하는 이터레이터를 반환
itertools.cycle(iter) # 이터러블 객체를 무한히 반복하는 이터레이터를 반환
itertools.chain(iter1, iter2 ... ) # 이터러블 객체들의 요소들을 순서대로 반환하는 이터레이터를 반환
itertools.compress(data iter, boolean iter) # True인 인덱스에 대하여 data 요소를 반환
...

 

 

itertools — 효율적인 루핑을 위한 이터레이터를 만드는 함수 — Python 3.8.17 문서

itertools — 효율적인 루핑을 위한 이터레이터를 만드는 함수 이 모듈은 APL, Haskell 및 SML의 구성물들에서 영감을 얻은 여러 이터레이터 빌딩 블록을 구현합니다. 각각을 파이썬에 적합한 형태로

docs.python.org


참고: Functional Programming HOWTO