점프 투 파이썬 DAY16(클로저와 데코레이터)

정지범·6일 전

점프 투 파이썬

목록 보기
30/37

07-2 클로저와 데코레이터

목차

  • 1. 클로저란?
  • 2. 클래스를 이용한 방법
  • 3. __call__ 메서드
  • 4. 클로저 만들기
  • 5. 데코레이터란?
  • 6. @를 이용한 데코레이터
  • 7. *args, `kwargs`**

1. 클로저란?

클로저(Closure) 는 쉽게 말하면

외부 함수가 종료된 이후에도 외부 함수의 변수를 기억하고 사용하는 내부 함수

이다.

말로만 보면 조금 어렵기 때문에 예제를 통해 이해해 보자.


항상 3을 곱하는 함수

어떤 숫자를 입력하면 항상 3을 곱해 주는 함수를 만들어 보자.

def mul3(n):
    return n * 3

사용

print(mul3(10))

실행 결과

30

이번에는 항상 5를 곱하는 함수가 필요하다고 해보자.

def mul5(n):
    return n * 5

사용

print(mul5(10))

결과

50

하지만 필요한 숫자가 계속 늘어난다면

mul3()
mul5()
mul6()
mul7()
mul8()
...

처럼 함수를 계속 만들어야 한다.

이것은 비효율적이다.


2. 클래스를 이용한 방법

먼저 클래스를 사용하면 이 문제를 해결할 수 있다.

class Mul:
    def __init__(self, m):
        self.m = m

    def mul(self, n):
        return self.m * n

객체를 만들어 보자.

mul3 = Mul(3)
mul5 = Mul(5)

여기서

Mul(3)

을 실행하면

self.m = 3

이 저장된다.

따라서

mul3.mul(10)

을 실행하면

self.m * n
3 * 10
= 30

이 된다.

전체 코드는 다음과 같다.

class Mul:
    def __init__(self, m):
        self.m = m

    def mul(self, n):
        return self.m * n


mul3 = Mul(3)
mul5 = Mul(5)

print(mul3.mul(10))
print(mul5.mul(10))

실행 결과

30
50

3. __call__ 메서드

앞에서 객체를 사용하려면

mul3.mul(10)

처럼 메서드를 직접 호출해야 했다.

하지만 __call__()을 사용하면 객체 자체를 함수처럼 호출할 수 있다.

class Mul:
    def __init__(self, m):
        self.m = m

    def __call__(self, n):
        return self.m * n

이제

mul3 = Mul(3)

으로 객체를 만든 뒤

print(mul3(10))

처럼 사용할 수 있다.

실행 결과

30

__call__ 동작 이해하기

다음 코드를 실행하면

mul3(10)

파이썬은 내부적으로

mul3.__call__(10)

을 호출한다고 생각하면 된다.

즉,

mul3(10)
    ↓
__call__(10)
    ↓
self.m * 10
    ↓
3 * 10
    ↓
30

이다.


4. 클로저 만들기

클래스를 사용하지 않고 함수만으로도 같은 기능을 만들 수 있다.

def mul(m):
    def wrapper(n):
        return m * n

    return wrapper

처음 보면 가장 헷갈리는 부분은 이것이다.

return wrapper

wrapper()를 실행하는 것이 아니라 함수 자체를 반환하고 있다.


함수를 변수에 저장할 수 있다

파이썬에서는 함수도 객체이기 때문에 변수에 저장할 수 있다.

예를 들어

def hello():
    print("Hello")

a = hello

라고 하면 a에는 hello 함수 자체가 저장된다.

따라서

a()

를 실행하면

Hello

가 출력된다.


5. 클로저 동작 과정

다시 다음 코드를 살펴보자.

def mul(m):
    def wrapper(n):
        return m * n

    return wrapper

그리고

mul3 = mul(3)

을 실행한다고 해보자.


① mul(3) 호출

mul(3)

이므로

m = 3

이 된다.


② 내부 함수 생성

def wrapper(n):
    return m * n

wrapper() 함수가 만들어진다.

이 함수는 외부 함수의 변수 m을 사용하고 있다.


③ wrapper 함수 반환

return wrapper

에 의해 wrapper 함수 자체가 반환된다.

결과적으로

mul3 = mul(3)

에서 mul3에는 wrapper 함수가 들어간다.


④ mul 함수 종료

여기서 중요한 점이 있다.

mul() 함수는 이미 실행이 끝났다.

그런데

mul3(10)

을 실행하면 결과가 어떻게 될까?

30

이 출력된다.


왜 m = 3이 남아 있을까?

원래라면 mul() 함수가 끝났으므로 내부 변수 m도 사라질 것처럼 보인다.

하지만 반환된 wrapper() 함수가 m을 사용하고 있기 때문에

m = 3

이라는 값을 기억하고 있다.

이것이 바로 클로저이다.

mul(3)
  ↓
m = 3
  ↓
wrapper 함수 생성
  ↓
wrapper가 m = 3을 기억
  ↓
mul 함수 종료
  ↓
mul3(10)
  ↓
3 × 10
  ↓
30

6. 여러 개의 클로저 만들기

다음처럼 사용할 수 있다.

def mul(m):
    def wrapper(n):
        return m * n

    return wrapper


mul3 = mul(3)
mul5 = mul(5)

print(mul3(10))
print(mul5(10))

실행 결과

30
50

여기서

mul3 = mul(3)

은 m = 3을 기억하고,

mul5 = mul(5)

는 m = 5를 기억한다.

즉 서로 다른 값을 기억하는 함수가 만들어진다.

mul3
 └─ m = 3 기억

mul5
 └─ m = 5 기억

📌 클로저 핵심

def outer(a):

    def inner(b):
        return a + b

    return inner

이 구조를 기억하면 된다.

외부 함수
   ↓
외부 변수 생성
   ↓
내부 함수 생성
   ↓
내부 함수가 외부 변수 사용
   ↓
내부 함수 반환
   ↓
외부 함수 종료
   ↓
내부 함수는 외부 변수를 계속 기억

7. 데코레이터란?

데코레이터(Decorator) 는

기존 함수의 코드를 직접 수정하지 않고 새로운 기능을 추가하는 방법

이다.

데코레이터를 이해하려면 앞에서 배운 클로저가 중요하다.


8. 함수 실행 시간 측정하기

다음 함수가 있다고 해보자.

def myfunc():
    print("함수가 실행됩니다.")

이 함수가 실행되는 시간을 측정하고 싶다면 time 모듈을 사용할 수 있다.

import time

def myfunc():
    start = time.time()

    print("함수가 실행됩니다.")

    end = time.time()

    print("함수 수행시간: %f 초" % (end - start))

실행

myfunc()

결과

함수가 실행됩니다.
함수 수행시간: 0.0000XX 초

문제점

함수가 한 개라면 괜찮다.

하지만 실행 시간을 측정해야 하는 함수가

func1()
func2()
func3()
func4()
...

처럼 많다면 모든 함수에

start = time.time()

...

end = time.time()

을 반복해서 작성해야 한다.

이런 경우 데코레이터를 사용할 수 있다.


9. 실행 시간을 측정하는 데코레이터 만들기

import time

def elapsed(original_func):

    def wrapper():
        start = time.time()

        result = original_func()

        end = time.time()

        print("함수 수행시간: %f 초" % (end - start))

        return result

    return wrapper

여기에서도 클로저와 같은 구조가 사용된다.

elapsed()
   ↓
wrapper() 생성
   ↓
wrapper 반환

하지만 이번에는 elapsed()가 함수를 입력값으로 받는다.

def elapsed(original_func):

10. 함수도 인수로 전달할 수 있다

다음 함수가 있다고 해보자.

def myfunc():
    print("함수가 실행됩니다.")

그리고

decorated_myfunc = elapsed(myfunc)

을 실행한다.

여기서 myfunc()라고 하지 않은 점이 중요하다.

elapsed(myfunc)

처럼 함수 자체를 전달했다.


동작 과정

decorated_myfunc = elapsed(myfunc)

실행

myfunc 함수
     ↓
elapsed()에 전달
     ↓
original_func = myfunc
     ↓
wrapper 함수 생성
     ↓
wrapper 함수 반환
     ↓
decorated_myfunc에 저장

이제

decorated_myfunc()

을 실행하면 실제로는 wrapper()가 실행된다.


wrapper 내부

def wrapper():

    start = time.time()

    result = original_func()

    end = time.time()

    print("함수 수행시간: %f 초" % (end - start))

    return result

여기서

original_func()

는 처음 전달했던

myfunc()

를 의미한다.

따라서

wrapper 실행
    ↓
시작 시간 저장
    ↓
myfunc 실행
    ↓
종료 시간 저장
    ↓
실행 시간 출력

이 된다.


11. 데코레이터 구조 이해하기

전체 코드를 다시 보면

import time

def elapsed(original_func):

    def wrapper():
        start = time.time()

        result = original_func()

        end = time.time()

        print("함수 수행시간: %f 초" % (end - start))

        return result

    return wrapper


def myfunc():
    print("함수가 실행됩니다.")


decorated_myfunc = elapsed(myfunc)

decorated_myfunc()

핵심은

elapsed(myfunc)

가 기존 함수 myfunc를 감싸는 새로운 wrapper 함수를 만들어 준다는 것이다.

그래서 데코레이터를

함수를 꾸며 주는 함수

라고 생각할 수 있다.


12. @를 이용한 데코레이터

파이썬에서는 위의 코드를 더욱 간단하게 표현할 수 있다.

@elapsed
def myfunc():
    print("함수가 실행됩니다.")

전체 코드는 다음과 같다.

import time

def elapsed(original_func):

    def wrapper():
        start = time.time()

        result = original_func()

        end = time.time()

        print("함수 수행시간: %f 초" % (end - start))

        return result

    return wrapper


@elapsed
def myfunc():
    print("함수가 실행됩니다.")


myfunc()

@elapsed의 의미

다음 코드는

@elapsed
def myfunc():
    print("함수가 실행됩니다.")

개념적으로 다음과 같다.

myfunc = elapsed(myfunc)

즉

기존 myfunc
      ↓
elapsed에 전달
      ↓
wrapper 반환
      ↓
myfunc에 다시 저장

된다.

따라서 이제

myfunc()

을 실행하면 단순히 원래 myfunc()만 실행되는 것이 아니라 wrapper()가 실행된다.


13. 데코레이터 실행 흐름

@elapsed
def myfunc():
    print("함수가 실행됩니다.")

그리고

myfunc()

을 실행하면

myfunc()
   ↓
wrapper() 실행
   ↓
시작 시간 기록
   ↓
원래 myfunc() 실행
   ↓
종료 시간 기록
   ↓
수행 시간 출력

이 된다.


14. 매개변수가 있는 함수에서는 문제가 발생한다

이번에는 myfunc()가 값을 입력받도록 변경해 보자.

@elapsed
def myfunc(msg):
    print("'%s'을 출력합니다." % msg)

사용

myfunc("You need python")

그런데 기존의 wrapper()는 다음과 같다.

def wrapper():

아무런 인수도 받을 수 없다.

그런데 우리는

myfunc("You need python")

처럼 인수를 전달하고 있다.

따라서 오류가 발생한다.

TypeError

15. *args와 **kwargs

이 문제를 해결하기 위해

*args

와

**kwargs

를 사용할 수 있다.


*args

*args는 여러 개의 일반 인수를 받을 수 있다.

def func(*args):
    print(args)

실행

func(1, 2, 3)

결과

(1, 2, 3)

즉 args는 튜플이 된다.

*args
  ↓
일반적인 여러 인수
  ↓
튜플

16. **kwargs

**kwargs는

이름=값

형태의 인수를 받을 수 있다.

def func(**kwargs):
    print(kwargs)

실행

func(name="pingu", age=20)

결과

{'name': 'pingu', 'age': 20}

즉 kwargs는 딕셔너리가 된다.

**kwargs
   ↓
key=value 형태의 인수
   ↓
딕셔너리

17. *args, **kwargs 함께 사용하기

def func(*args, **kwargs):
    print(args)
    print(kwargs)

실행

func(1, 2, 3, name="foo", age=3)

결과

(1, 2, 3)
{'name': 'foo', 'age': 3}

정리하면

매개변수받는 값저장 형태
*args일반 위치 인수튜플
**kwargskey=value 형태 인수딕셔너리

18. 데코레이터에 *args, **kwargs 적용하기

이제 wrapper()를 다음처럼 변경한다.

def wrapper(*args, **kwargs):

그리고 원래 함수를 실행할 때도 그대로 전달한다.

original_func(*args, **kwargs)

전체 코드는 다음과 같다.

import time

def elapsed(original_func):

    def wrapper(*args, **kwargs):

        start = time.time()

        result = original_func(*args, **kwargs)

        end = time.time()

        print("함수 수행시간: %f 초" % (end - start))

        return result

    return wrapper

이제 어떤 형태의 인수를 사용하는 함수라도 데코레이터를 적용하기 쉬워진다.


19. 최종 코드

import time


def elapsed(original_func):

    def wrapper(*args, **kwargs):

        start = time.time()

        result = original_func(*args, **kwargs)

        end = time.time()

        print("함수 수행시간: %f 초" % (end - start))

        return result

    return wrapper


@elapsed
def myfunc(msg):
    print("'%s'을 출력합니다." % msg)


myfunc("You need python")

실행 결과

'You need python'을 출력합니다.
함수 수행시간: 0.0000XX 초

20. 클로저와 데코레이터 차이

둘은 서로 관련이 있지만 완전히 같은 것은 아니다.

구분클로저데코레이터
핵심외부 함수의 값을 기억기존 함수에 기능 추가
구조내부 함수를 반환함수를 받아 새로운 함수를 반환
사용 목적상태나 값을 기억공통 기능 재사용
예시곱할 숫자 기억실행 시간 측정

데코레이터는 클로저 구조를 활용하는 대표적인 방법이라고 이해하면 쉽다.


21. 클로저 구조 다시 보기

def mul(m):

    def wrapper(n):
        return m * n

    return wrapper
mul(3)
  ↓
m = 3
  ↓
wrapper 생성
  ↓
wrapper가 m을 기억
  ↓
wrapper 반환
  ↓
mul3(10)
  ↓
30

핵심은

내부 함수가 외부 함수의 변수를 기억한다.


22. 데코레이터 구조 다시 보기

def decorator(original_func):

    def wrapper(*args, **kwargs):

        # 추가 기능

        result = original_func(*args, **kwargs)

        # 추가 기능

        return result

    return wrapper

사용

@decorator
def myfunc():
    pass

구조는

원래 함수
   ↓
데코레이터에 전달
   ↓
wrapper가 원래 함수 기억
   ↓
wrapper 반환
   ↓
원래 함수 대신 wrapper 실행

이라고 이해하면 된다.


📌 핵심 정리

클로저

def outer(a):

    def inner(b):
        return a + b

    return inner

inner()는 outer() 함수가 종료된 이후에도 a의 값을 기억한다.

외부 함수의 값
      ↓
내부 함수가 기억
      ↓
외부 함수 종료 후에도 사용 가능

__call__

객체를 함수처럼 사용할 수 있게 해주는 특수 메서드이다.

class Test:
    def __call__(self):
        print("실행")


a = Test()

a()

데코레이터

기존 함수를 수정하지 않고 기능을 추가한다.

@decorator
def myfunc():
    pass

개념적으로

myfunc = decorator(myfunc)

와 같은 구조이다.


*args

여러 개의 일반 인수를 받는다.

def func(*args):
    print(args)
튜플로 저장

**kwargs

key=value 형태의 여러 인수를 받는다.

def func(**kwargs):
    print(kwargs)
딕셔너리로 저장

💡 가장 중요한 흐름

클로저

외부 함수 실행
     ↓
외부 변수 생성
     ↓
내부 함수 생성
     ↓
내부 함수 반환
     ↓
외부 함수 종료
     ↓
내부 함수가 외부 변수 기억

데코레이터

기존 함수
   ↓
데코레이터에 전달
   ↓
wrapper 함수 생성
   ↓
기존 함수 + 추가 기능
   ↓
wrapper 반환
   ↓
함수 호출

클로저는 "값을 기억하는 함수", 데코레이터는 "기존 함수를 감싸서 기능을 추가하는 함수"라고 먼저 이해하면 훨씬 쉽다.

profile
성실하자:)

0개의 댓글