Skip to content

2.12. 내용정리: 12일차

흔한 찐따 edited this page Apr 1, 2022 · 16 revisions

함수형 패러다임 (Functional Paradigm)

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

함수의 여러 가지 형태

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

람다 함수 (Lambda Function)

  • 람다 함수는 이전에도 배웠던 개념이다.
  • 함수의 명칭을 지정하지 않아도 된다는 점에서 익명 함수(Anonymous Function) 라고도 한다.
  • 함수를 간단히 한줄만으로 만들게 해준다는 점에서 람다식(Lambda Expression) 이라고도 한다.
  • 주로 함수를 사용할 수 없는 경우나 간단한 함수를 인자값으로 넘길 때 사용된다.

예시

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 이 된다.

재귀 함수의 한계점

  • 재귀 함수의 성능은 좋지 않은 편이다.
  • 위의 코드가 수행되는 과정 중 스택 이라는 메모리 영역에 계속 쌓이는 개념이다.
  • 스택(Stack) 이란, 마지막에 들어온 요소가 가장 먼저 빠져 나오는 구조(LIFO; Last-In First-Out)를 지닌 자료 구조를 의미한다.
  • 컴퓨터 메모리 구조는 스택이라는 영역이 존재하는데, 이 스택이라는 영역에 호출된 함수를 차곡차곡 쌓아 올리면서 기억한다.
  • 쌓아올린 스택을 다시 회귀하면서 연산하는 과정이 지나치게 많아지게 되면 속도와 성능이 저하되는데, 이를 스택 오버헤드(Stack Overhead) 라고 부른다.
  • 또한, 스택이 계속 쌓여서 더 이상 쌓을 공간이 부족해 메모리 공간을 초과하게 되면, 스택 오버플로우(Stack Overflow) 에러가 발생한다.

즉, 위의 재귀 함수가 호출되는 과정을 좀 더 상세하게 나타내면 다음과 같다.

  1. 함수 factorial 이 호출된다.
  2. 메모리의 스택 영역에 호출된 함수를 기억하기 위해 호출된 함수를 쌓아 올린다.
  3. 인자값은 5 이므로, n5 가 된다.
  4. 51 보다 크기 때문에 5 * factorial(5 - 1)return 키워드에 의해 반환된다.
  5. 반환됨과 동시에 factorial(5 - 1) 이 호출되므로, factorial(5 - 1) 을 수행한다.
  6. 위의 과정을 계속 반복한다.
  7. 메모리의 스택 영역에 새롭게 호출된 factorial(5 - 1) 를 쌓아 올린다.
  8. 이 과정이 계속 반복되면 총 5번 쌓이게 된다. ( n5 이므로 5번 쌓인다. 만약 n10 이면 총 10번 호출하게 되므로, 10번 쌓인다.)
  9. 가장 마지막에 호출된 함수 factorial(2 - 1) 이 스택에서 먼저 빠져 나온다. (LIFO)
  10. 위와 같은 과정을 반복하면서 곰셈 연산을 위해 자신이 호출되었던 위치를 찾기 위해 회귀하는 과정을 거친다.
  11. 최종적으로 1 * 2 * 3 * 4 * 5 를 연산한 값이 반환된다.
  12. 따라서 x120 이 된다.

결론

  • 만약 n 값이 100 이었다면, 100 번 호출하게 되고, 스택 영역에서 다시 100 번 빠져나오면서 곱셈 연산까지 수행해야 한다.
  • 따라서 이렇게 단순한 재귀 함수는 성능이 좋지 못하다는 한계점이 있다.

꼬리 재귀 함수 (Tail Recursive Function)

  • 위에서 살펴본 재귀 함수의 한계점을 보완하기 위한 일종의 기법이다.
  • 꼬래 재귀 함수의 원리는 일반적인 재귀 함수처럼 추가적인 연산 과정을 하지 않는다.
  • 대신, 연산해야 하는 값을 자체적으로 기억하는 방식이다.
  • 단, 컴파일러(Compiler) 차원에서 꼬리 재귀의 최적화를 지원해야 한다는 조건이 있다.
    • 컴파일러(Compiler) 란, 프로그래밍 언어를 기계어로 바꿔주는 일종의 통역사 역할을 해주는 것이다.
    • 파이썬같은 스크립트 언어같은 경우, 컴파일러라는 말 대신에 인터프리터(Interpreter), 즉 해석기라고 표현한다.
    • CC++ , 그리고 Python 역시 컴파일러 차원에서 최적화를 지원한다.
    • JAVA 같은 경우, 컴파일러 차원에서 자체적인 지원은 하지 않는다고 한다.

예시

def factorial(n, i=1):
    if n > 1:
        return factorial(n - 1, n * i)
    else:
        return i
  • 일반 재귀 함수와의 차이점은 매개 변수가 두 개로 늘어났다는 점이다.
  • 요점은 재귀 호출 이후 추가적인 연산을 요구하지 않도록 구현하는 것이다.
  • 즉, 스택 영역에 쌓아 올린 후, 추가적인 곱셈 연산을 다시 한번 하지 않는다는 것에 있다.
  • 함수 호출 이후에 쌓아 올려진 스택을 빠져 나와 회귀하는 과정에서 추가적인 연산을 하지 않음으로, 오버헤드를 줄일 수 있다.

퍼스트 클래스 함수 (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)

콜백 함수 (Callback Function)와 이벤트 핸들러 (Event Handler)

  • 콜백 함수란, 다른 함수의 인자로써 넘겨진 후(퍼스트 클래스 함수) 특정 이벤트에 의해 호출되는 함수를 의미한다.
    • 이벤트(event) 는 말 그대로 어떤 사건을 의미한다.
    • 간단한 예시로, 키보드의 특정한 키를 입력하거나, 마우스 클릭과 같은 이벤트 등이 있다.
  • 주로 넘겨받는 인자값의 함수는 람다식으로 전달된다.
  • 콜백 함수를 실행하는 함수를 이벤트를 조율하는 함수라는 의미의 이벤트 핸들러(Event Handler) 라고 한다.

예시

  • 아래의 예시에서 이벤트 핸들러 함수 f 를 정의하였다.
  • 이때, 함수 f 의 인자값으로 받는 x 는 콜백 함수이며, y 는 정수를 받는다.
  • y0 보다 크면 함수 x 를 호출한 결과값을 반환한다.
  • y0 보다 작으면 'y'의 값이 0보다 작습니다. 라는 문자열을 반환한다.
# 이벤트 핸들러 함수 'f' 정의
def f(x, y):
    # 여기서 인자값으로 넘겨진 'x'는 함수이며, 이를 '콜백 함수'라고 한다.
    if y > 0:
        return x(y)
    else:
        return "'y'의 값이 0보다 작습니다."

그 뒤, 아래의 코드를 실행해본다.

n = input('수를 입력하세요:')
if n:
    n = int(n)
    y = f(lambda x: x * 2, n)
    print(y)

설명

  • 특정한 이벤트가 발생했을 때(위의 코드같은 경우, 수를 입력받았을 때) 아래의 함수가 수행된다.
  • 만약 사용자로부터 입력받은 결과가 존재하는 경우, 즉 None 이 아닐 경우, 이벤트 핸들러 함수 f 를 호출한다.
  • 함수 f 는 퍼스트 클래스 함수이자, 이벤트 핸들러이다.
  • 인자값으로 넘겨받은 람다식 lambda x: x * 2 는 어떤 수에 2 를 곱셈 연산을 하는 함수이다.

중첩 함수 (Nested function)

  • 중첩 함수란, 말 그대로 함수 내에 또 다른 함수를 의미한다.
  • 함수 내부에 선언된 함수이므로, 내부 함수(Inner function) 라고도 한다.
  • 기존함수를 한번 감싸서 원래 동작에 약간의 처리를 추가하는 함수를 의미하는 래퍼 함수(Wrapper 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 를 호출한다.

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

매개 변수와 반환값을 처리하는 데코레이터

매개 변수와 반환값을 처리해주는 데코레이터 역시 정의하는 게 가능하다.

# 호출할 함수를 매개 변수로 받는다.
def trace(func):
    # 호출할 함수 'add(a, b)'의 매개 변수와 똑같이 지정한다.
    def wrapper(a, b):
        # func에 매개 변수 'a'와 'b'를 넣어서 호출하고 반환값을 변수에 저장한다.
        result = func(a, b)
        # 매개 변수와 반환값 출력
        print(f'{func.__name__}(a={a}, b={b}) -> {result}'
        return result
    # 위에서 정의한 내부 함수 'wrapper'를 반환한다.
    return wrapper

# @데코레이터
@trace
# 매개 변수는 'a'와 'b' 총 두 개이다.
def add(a, b):
    return a + b

result = add(10, 20)
print(result)

결과

add(a=10, b=20) -> 30
30
  • 함수 add 함수를 호출했을 때 데코레이터를 통해서 매개 변수와 반환값이 출력되었다.
  • 매개 변수와 반환값을 처리하는 데코레이터를 만들 때는 먼저 내부에 정의한 함수 wrapper 의 매개 변수를 호출할 함수 add(a, b) 의 매개 변수와 똑같이 만들어준다.
  • 함수 wrapper 안에서는 func 를 호출하고, 반환값을 변수에 저장한다.
  • 그 다음, print 로 매개 변수와 반환값을 출력한다.
  • 이때 func 에는 매개 변수 ab 를 그대로 넣어준다.
  • 또한, 함수 add 는 두 수를 더해서 반환해야 하므로 func 의 반환값을 return 키워드로 반환해준다.
  • 데코레이터를 사용할 때는 @ 기호로 함수 위에 지정해주면 된다.
  • 또한, @ 기호로 데코레이터를 사용했으므로, 함수 add 는 그대로 호출해준다.

가변 인수 함수 데코레이터

인자값의 개수가 정해지지 않는 가변 인수를 받는 함수 역시 데코레이터로 받는 것이 가능하다.

# 호출할 함수를 매개 변수로 받는다.
def trace(func):
    # 가변 인수 함수를 정의한다.
    def wrapper(*args, **kwargs):
        # 'func'에 가변 인자 'args'와, 키워드 가변 인자 'kwargs'를 언패킹하여 넣어준다.
        result = func(*args, **kwargs)
        print(f'{func.__name__}(args={args}, kwargs={kwargs}) -> {result}'
    return wrapper

@trace
def get_max(*args):
    return max(args)

@trace
def get_min(**kwargs):
    return min(kwargs.values())

print(get_max(10, 20))
print(get_min(x=10, y=20, z=30))

결과

get_max(args=(10, 20), kwargs={}) -> 20
20
get_min(args=(), kwargs={'x': 10, 'y': 20, 'z': 30}) -> 10
10

데코레이터를 여러 개 지정하기

  • 함수에는 데코레이터를 여러 개 지정할 수 있다.
  • 다음과 같이 함수 위에 데코레이터를 여러 줄로 지정해준다.
  • 이때, 데코레이터가 실행되는 순서는 위 -> 아래 순이다.
@데코레이터1
@데코레이터2
def 함수명():
    코드

예시

def decorator1(func):
    def wrapper():
        print('decorator1')
        func()
    return wrapper
 
def decorator2(func):
    def wrapper():
        print('decorator2')
        func()
    return wrapper
 
# 데코레이터를 여러 개 지정
@decorator1
@decorator2
def hello():
    print('hello')
 
hello()

결과

decorator1
decorator2
hello

@ 기호를 사용하여 데코레이터로 정의하지 않았을 때는 다음 코드와 동작이 같다.

decorated_hello = decorator1(decorator2(hello))
decorated_hello()

클래스로 데코레이터 만들기

  • 클래스로 데코레이터를 직접 정의할 수 있다.
  • 특히 클래스를 활용할 때는 인스턴스를 함수처럼 호출하게 해주는 매직 메서드 __call__ 을 구현해야 한다.

예시

아래는 클래스를 통해 데코레이터를 정의하는 예시이다.

class Trace:
    # 호출할 함수를 인스턴스의 초기값으로 받는다.
    def __init__(self, func):
        # 호출할 함수를 속성 'func'에 할당한다.
        self.func = func
 
    def __call__(self):
        # '__name__'으로 함수 이름을 출력한다.
        print(self.func.__name__, '함수 시작')
        # 속성 'func'에 저장된 함수를 호출한다.
        self.func()
        print(self.func.__name__, '함수 끝')


# 정의한 데코레이터를 사용한다.
@Trace
def hello():
    print('hello')


# 함수를 그대로 호출한다.
hello()

결과

hello 함수 시작
hello
hello 함수 끝

클래스로 매개 변수와 반환값을 처리하는 데코레이터 만들기

클래스로 만든 데코레이터도 매개변수와 반환값을 처리할 수 있다.

예시

  • 아래는 함수의 매개 변수를 출력하는 데코레이터를 정의한 예시이다
  • 여기서는 위치 인수와 키워드 인수를 모두 처리하는 가변 인수로 만들었다.
class Trace:
    # 호출할 함수를 인스턴스의 초기값으로 받는다.
    def __init__(self, func):
        # 호출할 함수를 속성 'func'에 할당한다.
        self.func = func

    # 호출할 함수의 매개변수를 처리한다.
    def __call__(self, *args, **kwargs):
        # 'self.func'에 매개 변수를 받아서 호출하고 반환값을 변수에 할당한다.
        result = self.func(*args, **kwargs)
        print(f'{self.func.__name__}(args={args}, kwargs={kwargs}) -> {result}')


@Trace
def add(a, b):
    return a + b


print(add(10, 20))
print(add(a=10, b=20))

결과

add(args=(10, 20), kwargs={}) -> 30
30
add(args=(), kwargs={'a': 10, 'b': 20}) -> 30
30
  • 클래스로 매개변수와 반환값을 처리하는 데코레이터를 만들 때는 매직 메서드 __call__ 에 매개 변수를 지정한다.
  • 그 뒤에 self.func 에 매개 변수를 받아서 호출한 뒤에 반환값을 반환해주면 된다.
  • 위의 예제에서는 매개 변수를 *args , **kwargs 로 지정했으므로, self.func 으로 받을때는 언패킹하여 넣어준다.
  • 물론 가변 인수를 사용하지 않고, 고정된 매개 변수를 사용할 때는 def __call__(self, a, b): 처럼 정의해도 된다.

클래스로 매개 변수가 있는 데코레이터 만들기

마찬가지로, 클래스로 매개 변수가 있는 데코레이터를 정의할 수 있다.

예시

class IsMultiple:
    # 데코레이터가 사용할 매개 변수를 초기값으로 받는다.
    def __init__(self, x):
        # 매개변수를 속성 x에 저장
        self.x = x

    # 호출할 함수를 매개변수로 받는다.
    def __call__(self, func):
        # 호출할 함수의 매개변수와 똑같이 지정한다.
        # 물론, 가변 인수로 작성해도 가능하다.
        def wrapper(a, b):
            # 인자로 받은 함수 'func'를 호출하고 반환값을 변수에 할당한다.
            result = func(a, b)
            # 인자로 받은 함수 'func'의 반환값이 'self.x'의 배수인지 확인한다.
            if result % self.x == 0:
                print(f'{func.__name__}의 반환값은 {self.x}의 배수입니다.')
            else:
                print(f'{func.__name__}의 반환값은 {self.x}의 배수가 아닙니다.')

            # 인자로 받은 함수 'func'의 반환값을 반환한다.
            return result

        # 내부 함수 'wrapper'를 반환한다.
        return wrapper

# 데코레이터(인수)
@IsMultiple(3)
def add(a, b):
    return a + b

print(add(10, 20))
print(add(2, 5))

결과

add의 반환값은 3의 배수입니다.
30
add의 반환값은 3의 배수가 아닙니다.
7

지금까지 생성자 __init__ 에서 호출할 함수를 매개 변수로 받았는데, 여기서는 데코레이터가 사용할 매개변수를 받는다.

함수형 패러다임 활용하기

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

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