Skip to content

2.12. 내용정리: 12일차

흔한 찐따 edited this page Mar 19, 2022 · 16 revisions

함수형 패러다임 (Functional Paradigm)

  • 파이썬은 객체지향 패러다임만을 제공하는 것이 아니라 다양한 프로그래밍 패러다임을 제공한다.
  • 파이썬에서는 수많은 패러다임 중 함수형 패러다임 역시 제공한다.
  • 함수형 프로그래밍(functional programming) 은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다.
  • 객체지향 프로그래밍은 객체를 중심적으로 사고하는 기법이라면, 함수형 프로그래밍은 거의 모든 것을 순수 함수로 나누어 문제를 해결하는 기법 이다.
  • 작은 문제를 해결하기 위한 함수를 작성하여 가독성을 높이고 유지 보수를 용이하게 해준다.

함수의 여러 가지 형태

  • 파이썬에는 여러 가지 형태의 함수들이 존재한다.
  • 앞서 파이썬에는 함수형 패러다임을 제공한다고 서술했었다.
  • 함수형 패러다임에서 사용되는 몇 가지 함수 패턴들이 존재한다.

람다 함수 (Lambda Function)

  • 람다 함수는 이전에도 배웠던 개념이다.
  • 간단한 함수를 한 줄만으로 만들게 해준다.
  • 주로 함수를 사용할 수 없는 경우나 간단한 함수를 인자값으로 넘길 때 사용된다.

예시

f = lambda x: x + 1
f(10)

재귀 함수 (Recursive Function)

재귀 함수란, 정의한 함수에 같은 함수를 호출하여 반복하는 결과를 만들어내는 함수를 의미한다.

예시

아래는 팩토리얼(factorial) 연산을 수행하는 함수 factorial 을 정의한 것이다.

def factorial(n):
    # 'n'이 1보다 클 경우에는 n과 factorial 함수를 다시 호출한 결과값을 곱셈한다.
    if n > 1:
        return n * factorial(n - 1)
    # 'n'이 1과 같거나 작다면 1을 반환시킨다.
    else:
        return 1

x = factorial(5)
print(x)

위의 코드가 수행되는 과정은 다음과 같다.

  1. 함수 factorial 이 호출된다.
  2. 인자값은 5 이므로, n5 가 된다.
  3. 51 보다 크기 때문에 5 * factorial(5 - 1)return 키워드에 의해 반환된다.
  4. 반환됨과 동시에 factorial(5 - 1) 이 호출되므로, factorial(5 - 1) 을 수행한다.
  5. n4 이므로, 다시 4 * factorial(4 - 1) 이 수행된다.
  6. 위와 같은 과정이 계속 반복이 되다보면 결국 n1 이 된다.
  7. n1 이므로, 1 이 반환된다.
  8. 최종적으로 5 * 4 * 3 * 2 * 1 을 반환하게 된다.
  9. 따라서 x120 이 된다.

퍼스트 클래스 함수 (First-class Function)

  • 퍼스트 클래스 함수란, 프로그래밍 언어가 함수를 일급 객체(first-class object 혹은 일등 시민; first-class citizen 라고도 함) 으로 취급하는 것을 의미한다.
  • 함수 자체를 인자(argument)로써 다른 함수에 전달하거나 다른 함수의 결과값으로 반환할 수도 있다.
  • 혹은 함수를 변수에 할당하거나 자료 구조안에 저장할 수 있는 함수를 의미한다.
  • 즉, 변수에 담을 수 있고, 함수의 인자로 전달하고 함수의 반환값(return value)으로 전달할 수 있는 함수를 의미한다.

일급 객체 (First-class Object)

  • 일급 객체란, 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.
  • 보통 함수에 인자로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 한다.

특징

  • 변수나 자료 구조 안에 담을 수 있다.
  • 파라미터로 전달 할 수 있다.
  • 반환값으로 사용할 수 있다.
  • 할당에 사용된 이름과 무관하게 고유한 구별이 가능하다.

예시

함수를 인자값으로 받는 함수 f 를 선언한다.

# 퍼스트 클래스 함수 'f' 선언
def f(x):
    # 여기서 파라미터 'x'는 함수이다.
    # 함수를 호출할 때에는 괄호를 사용한다.
    print("함수 'f' 호출")
    x()

그 다음, 함수 'test' 호출 이라는 메시지를 호출하는 함수 test 를 선언한다.

def test():
    print("함수 'test' 호출")

함수 f 에 인자값으로 함수 test 를 넣어주고 호출한다.

f(test)

중첩 함수 (Nested function)

  • 중첩 함수란, 말 그대로 함수 내에 또 다른 함수를 의미한다.
  • 함수 내부에 선언된 함수이므로, 내부 함수(Inner function) 라고도 한다.

예시

아래는 outer 라는 함수 안에 inner 라는 내부 함수를 선언하는 예시이다.

def outer():
    print("외부 함수 영역")

    def inner():
        print("내부 함수 영역")

    # 내부 함수 'inner' 호출
    inner()

# 함수 'outer' 호출
outer()

nonlocal

  • 이전에 배웠던 global 이라는 키워드는 함수 외부에 선언되어있는 변수(즉, 전역 변수)를 사용하기 위한 키워드이다.
  • nonlocal 키워드는 global 키워드와는 다르게, 중첩 함수의 관계에서, 내부 함수가 외부 함수의 지역 변수의 값을 다시 할당하려고 할 때 쓰이는 키워드이다.

예시

아래의 코드를 실행하면 UnboundLocalError 라는 에러가 발생한다.

def outer():
    a = 10

    def inner():
        a += 10
        print('a:', a)
    inner()

# 함수 'outer'를 호출하면 아래와 같은 에러를 발생시킨다.
# UnboundLocalError: local variable 'a'
outer()
  • 에러가 발생하는 이유는 할당하기 전에 지역 변수 a 가 이미 참조되었기 때문이다.
  • 즉, 변수 a 가 외부 함수의 지역 변수로써의 a 인지, 내부 함수의 지역 변수로써의 a 인지를 구별하지 못하고 있는 것이다.
    • 이전에 공부했었던 이름 공간이라는 개념을 생각해보면 된다.
  • 이러한 문제를 해결하기 위해서 변수 a 를 아래와 같이 nonlocal 로 선언하면 된다.
def outer():
    print("외부 함수 영역")
    a = 10

    # 내부 함수 'inner' 호출
    def inner():
        print("내부 함수 영역")

        # nonlocal 키워드를 사용하여 함수 'outer' 영역에 선언된 변수 'a'를 사용하겠다는 의미이다.
        nonlocal a
        a += 10
        print('a:', a)

    # 내부 함수 'inner' 호출
    inner()

# 함수 'outer' 호출
outer()

클로저 함수 (Closer Function)

  • 클로저 함수란, 어떤 함수를 함수 자신이 가지고 있는 환경과 함께 저장하는 함수이다.
  • 또한 함수가 가진 프리 변수(free variable) 를 클로저 함수가 만들어지는 당시의 값과 참조된 값들을 맵핑(mapping)해주는 역할을 한다.
    • 파이썬에서 프리 변수란, 코드 블럭안에서 사용은 되었지만, 그 코드 블럭안에서 정의되지 않은 변수를 의미한다.
  • 클로저 함수는 일반 함수와는 다르게, 자신의 영역 밖에서 호출된 함수의 변수값과 참조된 값들을 복사하고 저장한 뒤, 이 값들에 접근할 수 있게 도와준다.
  • 즉, 간단히 말해서 클로저 함수란, 자신이 가지고 있는 환경에 맞춰진 형태로 반환해주는 함수이다.

예시

아래는 클로저 함수 outer 와 내부 함수인 inner 를 실행시킨 결과값을 반환하는 예시이다.

# 클로저 함수 'outer' 정의
def outer():
    # 함수 'outer' 영역의 변수 'msg'
    msg = 'Hi'

    # 내부 함수 'inner' 정의
    def inner():
        # 내부 함수 'inner' 역시 함수 'outer' 영역에 있다.
        # 재선언을 하는 것이 아니기 때문에 함수 'outer' 영역의 변수 'msg'임을 알 수 있다.
        # 따라서 지역 변수 'msg'를 참조할 수 있다. (프리 변수)
        print(msg)

    # 함수 'inner'를 호출하면서 실행 결과를 반환시킨다.
    # 반환되는 값이 없으므로, 결과값은 'None'이 반환된다.
    return inner()

# 클로저 함수 'outer' 호출
outer()

응용

다음과 같이 함수를 반환시키는 함수를 만들어서 응용할 수도 있다.

# 클로저 함수 'f' 정의
def f(x):
    # 내부 함수 'g' 정의
    def g(y):
        # 클로저 함수 'f'의 파라미터 'x'의 값과 내부 함수 'g'의 파라미터 'y'의 값을 더해준다.
        return x + y
    # 내부 함수 'g'를 반환한다.
    return g

# 함수 'f'는 함수를 반환시키는 함수이므로, 아래와 같이 호출할 수 있다.
# 아래와 같은 방식을 마치 사슬처럼 엮여있는 모양이라고 해서 체이닝(chaining) 기법이라고 한다.
f(1)(2)

위와 같은 방식을 마치 사슬처럼 엮여있는 모양이라고 해서 체이닝(chaining) 기법이라고 한다.

데코레이터 (Decorator)

  • 데코레이터란, 함수와 메서드를 장식(decorate)하는 문법적인 요소이다.
  • 사용할 때에는 @ 기호를 붙여서 사용한다.

이해하기 쉽게 정의하자면 다음과 같다.

  • 이전에 lambda 함수같은 경우, 함수의 간단한 표현 방식이라고 서술하였다.
  • 데코레이터는 함수를 인자로 받는 함수, 즉 퍼스트 클래스 함수를 문법적인 요소로 간편하게 표현한 것이다.

데코레이터 함수 선언하기

  1. 함수를 인자로 받는 함수를 선언한다.
  2. 함수 안에 또다른 함수(내부 함수)를 정의한다.
  3. 새롭게 정의한 내부 함수를 반환한다.
  4. 이렇게 정의한 함수를 @ 기호를 붙여 @함수명 으로 새롭게 정의한 함수 윗줄에 추가한다.

예시

먼저, 아래의 예시처럼 함수를 인자로 받는 클로저 함수 f 를 정의한다.

def f(x):
    print("함수 'f' 호출")

    # 내부 함수 'g'를 정의한다.
    def g(y):
        print("함수 'g' 호출")
        # 함수를 인자로 받았기 때문에 'function' 타입이 출력된다.
        print(x)
        print(type(x))

        # 인자값으로 받은 함수 'x'에 함수 'g'의 인자값 'y'를 넘긴다.
        return x(y)

    # 내부 함수 'g'를 반환시킨다.
    return g

그 다음 새롭게 정의할 함수 윗줄에 @ 기호를 붙여 다음과 같이 정의한다.

@f
def f2(x):
    print("함수 'f2' 호출")
    return x + 1

그 다음 아래의 코드를 실행시켜본다.

y = f2(10)
print(y)

결과와 설명

위의 코드를 실행하면 결과는 다음과 같이 출력된다.

함수 'f' 호출
함수 'g' 호출
<function f2 at 0x.....>
<class 'function'>
함수 'f2' 호출
11

과정은 다음과 같다.

  1. 함수 f 에 인자값으로 x 가 넘어가며, x 는 함수이다.
  2. 내부 함수 g 에 인자값으로 받은 함수 x 가 넘어간다.
  3. 내부 함수 g 에서 인자값으로 넘긴 함수 x 에 내부 함수 g 의 인자값 y 를 넘긴다.
  4. 내부 함수 g 는 최종적으로 인자값으로 넘긴 함수 x 를 실행시킨 결과값을 반환시킨다.
  5. 결과적으로, 함수 f 는 위의 과정을 거쳐 만들어진 내부 함수 g 를 반환한다.
  6. 함수 f 가 호출된다.
  7. 함수 f 의 인자값 xf2 가 넘어간다.
  8. 내부 함수 g 에서 인자값으로 받은 함수 f2 에 인자값 y 를 넘기면서 함수 f2 를 호출시킨 후, 그 결과값을 반환되도록 만들어진다.
  9. 이렇게 만들어진 내부 함수 g 를 반환한다.
  10. 내부 함수 g 가 호출된다.
  11. 최종적으로, 내부 함수 g 는 함수 f2 를 호출한다.

과정을 전부 풀어서 보면 매우 복잡해 보이지만, 알고 보면 사실 굉장히 단순하다.

함수형 패러다임 활용하기

위에서 살펴본 퍼스트 클래스 함수와 클로저 함수 등을 활용한 다양한 기법들이 존재하며, 파이썬에서 이를 지원하는 대표적인 세 가지 함수가 존재한다.

map

  • map 함수는 순환 가능한(이터러블; iterable) 객체에 있는 모든 요소(element)에 인자값으로 넘겨받은 함수를 적용하여 그 결과를 반환한다.
    • 순환 가능한 객체, 즉 이터러블(iterable) 객체는 컨테이너 타입 중 인덱싱과 슬라이싱이 가능한 객체를 의미한다.
  • 이때 함수는 여러 인자값을 받을 수 있어야 하고, 모든 이터러블 객체의 요소에 동시에 적용되도록 해야 한다.
  • 즉, 정리하자면 map 함수는 어떤 순환 가능한 객체(대표적으로 리스트와 같은 컨테이너 타입)에 함수를 적용시킨 결과가 나오도록 해주는 함수이다.
  • 사용하는 방법은 map(순환 가능한 객체의 요소 하나를 인자로 받는 함수, 순환 가능한 객체) 이다.

예시

아래는 map 함수를 사용해 순환 가능한 객체인 리스트 l 에 있는 요소들을 모두 1 씩 증가시키는 예시이다.

def f(x):
    return x + 1

l = [1, 2, 3, 4, 5]
y = map(f, l)

# 출력하면 'map' 객체가 출력된다.
print(z)

map 함수를 통해 나온 결과인 map 타입의 객체는 자체적으로 사용이 불가능하기 때문에 타입을 변환시켜서 사용해야 한다.

z = list(y)

# 출력하면 모든 요소가 하나씩 더해진 '[2, 4, 6, 8, 10]'이 출력된다.
print(z)

위의 예시를 lambda 를 활용하면 훨씬 더 간단히 표현할 수 있다.

l = [1, 2, 3, 4, 5]
y = list(map(lambda x: x + 1, l))
print(y)

filter

  • filter 함수는 이터러블 객체의 각 요소에 대해 함수의 결과값이 True 를 반환하는 요소만을 추려내는 함수이다.
  • 즉, '필터'라는 이름에서 알 수 있듯, 어떤 조건(함수)에 만족하는 경우에 해당하는 요소들만을 추출해주는 함수이다.
  • 사용하는 방법은 map 함수와 동일하게 filter(함수, 순환 가능한 객체) 와 같이 사용한다.
    • 여기서 인자값으로 받는 함수의 결과값은 참과 거짓을 판단하는 함수가 들어간다.

예시

아래는 filter 함수를 활용해서 리스트 l 안에 있는 요소들 중에서 짝수에 해당하는 요소들만 가져오는 예제이다.

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

l = [1, 2, 3, 4, 5]
y = filter(f, l)

# 출력하면 'filter' 객체가 나온다.
print(y)

# 따라서 'map' 함수처럼 타입을 변환시켜서 사용해야 한다.
z = list(y)

# 짝수에 해당하는 값인 '[2, 4]'가 출력된다.
print(z)

마찬가지로, lambda 키워드를 통해 좀 더 축약하여 표현할 수 있다.

l = [1, 2, 3, 4, 5]
y = list(filter(lambda x: x % 2 == 0, l))
print(y)

reduce

  • 파이썬이 3.x 버전으로 업데이트가 진행되면서 reduce 함수가 파이썬의 기본적인 내장 함수에서 빠졌다고 한다.
  • 대신 파이썬에 내장된 functools 이라는 모듈(라이브러리)를 import 해서 사용할 수 있다.
    • reduce 함수를 사용하기 위해서는 from functools import reduce 를 통해 사용할 수 있다.
  • reduce 함수는 순환 가능한 객체의 각 요소를 왼쪽부터 오른쪽 방향으로 함수를 적용시키며 하나의 값으로 합쳐진 결과를 반환시켜준다.
  • 사용하는 방법은 reduce(함수, 순환 가능한 객체, 초기값<생략 가능>) 이다.

예시

아래는 reduce 함수를 활용해서 1 부터 100 까지 모두 더해 결과를 얻는 예시이다.

# reduce 함수를 사용하기 위해 아래의 코드를 추가한다.
from functools import reduce

def f(x, y):
    return x + y

l = [i for i in range(1, 101)]

# 함수의 결과값이 나오므로, 타입을 변환시킬 필요는 없다.
y = reduce(f, l)
print(y)

마찬가지로, lambda 를 통해 축약된 표현이 가능하다.

from functools import reduce

l = [i for i in range(1, 101)]
y = reduce(lambda x, y: x + y, l)
print(y)

초기값을 지정해주면 순환 가능한 객체안에 요소가 아무것도 없는 경우, 그 초기값을 반환시켜준다.

from functools import reduce

# 요소가 하나도 없는 빈 리스트이다.
l = []
# 초기값을 1로 지정한다.
y = reduce(lambda x, y: x + y, l, 1)
# 인자값으로 받은 리스트가 비어있으므로, y의 값은 초기값으로 지정한 1이 된다.
print(y)

위의 예시에서도 알 수 있듯, reduce(lambda x, y: x + y, [1, 2, 3, 4, 5]) 라고 한다면, ((((1+2)+3)+4)+5) 의 값을 반환하는 원리이다.

함수형 패러다임의 장점

위에서 살펴본 개념들을 토대로 장점들을 정리해보자면 다음과 같이 정리할 수 있다.

  • 프로그래밍 코드를 수학처럼 수식화시켜서 표현하는데 좋다.
    • 표현이 간결해지므로 코드가 짧아진다.
    • 따라서 가독성이 좋아지고 유지 보수하기가 보다 용이해진다.
  • 작은 문제들(위의 예제들에서도 살펴보았듯, 단순 반복하는 작업을 통해 결과를 내놓는 문제들)을 해결하기에 적합하다.
    • 위의 예제들에서도 살펴보았듯, 불필요한 반복문들이 많이 생략된다.
    • 자료 구조(컨테이너 타입)를 제자리에서 수정하여 해결한다.
  • 함수 자체가 독립적이므로 프로그램을 동작시키는데 안전성을 보장받을 수 있다.
    • 이는 쓰레드(Thread)와 같이 동시성 프로그래밍이나 멀티 프로그래밍 환경에서 빛을 발한다.
    • (이 개념에 대해 잘 모르겠지만, 한번에 여러 동작을 수행해야 하는 환경에서 유리하다고 이해했다.)

흔한 찐따

안녕하세요, 흔한 찐따 입니다.

Clone this wiki locally