1. 클로저(Closure)
클로저는 간단히 말해 함수 안에 내부 함수(inner function)를 구현하고 그 내부 함수를 리턴하는 함수를 말한다. 이때 외부 함수는 자신이 가진 변숫값 등을 내부 함수에 전달할 수 있다.
- 함수 안의 함수를 결과로 반환할 때, 그 내부함수를 클로저라고 함
- 콜백함수, 함수의 순차적 실행, 데코레이터 함수에 사용
def mul2(n):
return n * 2
print(mul2(10)) # 20
print(mul2(5)) # 10
def mul5(n):
return n * 5
mul5(10) # 50
mul5(5) # 25
# mul1, mul2, mul3, ... mul100도 만들어야 하나?
class Mul:
def __init__(self, m):
self.m = m
def mul(self, n):
return self.m * n
mul2 = Mul(2)
print(mul2.mul(10)) # 20
print(mul2.mul(5)) # 10
mul5 = Mul(5)
print(mul5.mul(10)) # 50
print(mul5.mul(5)) # 25
클래스를 이용하면 이 코드처럼 특정 값을 미리 설정하고 그다음부터 mul() 메서드를 사용하면 원하는 형태로 호출할 수 있다. 그리고 다시 다음과 같이 __call__ 메서드를 이용하여 이를 개선할 수 있다.
class Mul:
def __init__(self, m): # 객체를 생성할 때 호출
print('생성자 호출')
self.m = m
def __call__(self, n): # 객체를 실행할 때 호출
print('call 호출')
return self.m * n
mul2 = Mul(2) # 생성자 호출
mul2(10)
# call 호출
# 20
mul5 = Mul(5) # 생성자 호출
mul5(10)
# call 호출
# 50
mul() 함수 이름을 __call__로 바꾸었다. __call__ 함수는 Mul 클래스로 만든 객체에 인수를 전달하여 바로 호출할 수 있도록 하는 메서드이다. __call__ 메서드를 이용하면 이 예제처럼 mul3 객체를 mul3(10)처럼 호출할 수 있다. 이렇게 클래스로 만드는 방법이 일반적이긴 하지만, 더 간편한 방법이 있다.
# 클로저 사용하기
def mul(m): # 외부 함수
def wrapper(n): # 내부 함수(클로저)
return m * n
return wrapper
if __name__ == "__main__":
mul2 = mul(2) # m = 2인 wrapper 함수가 mul2에 저장
mul5 = mul(5)
print(mul2(10)) # 20
print(mul5(10)) # 50
외부 함수(mul()) 안에 내부 함수(wrapper())를 구현했다. 그리고 외부 함수는 내부 함수 wrapper()를 리턴한다. 함수가 함수를 리턴하는 것이 생소할 수 있겠지만 파이썬에서는 이것이 가능하다.
재밌는 사실은 mul() 함수에서 wrapper() 함수를 리턴할 때 mul() 함수 호출 시 인수로 받은 m값을 wrapper() 함수에 저장하여 리턴한다는 점이다. 이것은 마치 클래스가 특정한 값을 설정하여 객체를 만드는 과정과 매우 비슷하다. 이러한 mul()과 같은 함수를 파이썬에서는 클로저(Closure)라 한다.
2. 데코레이터(Decorator)
- 함수를 꾸며주는 함수
- 함수를 인수로 받는 클로저
- @(어노테이션)을 이용하여 사용
- 반복되는 작업을 여러 함수에 적용할 경우
- 기존 함수를 수정하지 않고 추가 기능을 구현하고 싶은 경우
클로저를 이용하면 기존 함수에 뭔가 추가 기능을 덧붙이기가 아주 편리하다. 이렇게 기존 함수를 바꾸지 않고 추가 기능을 덧붙일 수 있도록 하는 elapsed() 함수와 같은 클로저를 데코레이터(Decorator)라 한다.
import time
def func1(a, b):
print('함수가 시작되었습니다.')
start = time.time()
result = a + b
end = time.time()
print('함수가 종료되었습니다.')
print(f'함수 수행시간: {end - start}')
return result
def func2(a, b):
print('함수가 시작되었습니다.')
start = time.time()
result = a * b
end = time.time()
print('함수가 종료되었습니다.')
print(f'함수 수행시간: {end - start}')
return result
result = func1(10, 3)
# 함수가 시작되었습니다.
# 함수가 종료되었습니다.
# 함수 수행시간: 4.76837158203125e-07
result = func2(10, 3)
# 함수가 시작되었습니다.
# 함수가 종료되었습니다.
# 함수 수행시간: 4.76837158203125e-07
# 데코레이터 만들기
def func1(a, b):
result = a + b
return result
def func2(a, b):
result = a * b
return result
def elapsed(func):
def wrapper(a, b):
print('함수가 시작되었습니다.')
start = time.time()
result = func(a,b)
end = time.time()
print('함수가 종료되었습니다.')
print(f'함수 수행시간: {end - start}')
return result
return wrapper
deco1 = elapsed(func1)
result1 = deco1(10, 3)
print(result1)
# 함수가 시작되었습니다.
# 함수가 종료되었습니다.
# 함수 수행시간: 2.1457672119140625e-06
# 13
deco2 = elapsed(func2)
result2 = deco2(10, 3)
print(result2)
# 함수가 시작되었습니다.
# 함수가 종료되었습니다.
# 함수 수행시간: 1.6689300537109375e-06
# 30
함수 바로 위에 @elapsed(@+데코레이터 함수명)라는 어노테이션을 추가했다.
파이썬은 함수 위에 어노테이션이 있으면 데코레이터 함수로 인식한다.
@elapsed
def func1(a, b):
result = a + b
return result
@elapsed
def func2(a, b):
result = a * b
return result
result1 = func1(10, 3)
print(result1)
# 함수가 시작되었습니다.
# 함수가 종료되었습니다.
# 함수 수행시간: 1.6689300537109375e-06
# 13
result2 = func2(10, 3)
print(result2)
# 함수가 시작되었습니다.
# 함수가 종료되었습니다.
# 함수 수행시간: 1.9073486328125e-06
# 30
데코레이터 함수는 기존 함수의 입력 인수에 상관없이 동작하도록 해야 한다. 왜냐하면 데코레이터는 기존 함수가 어떤 입력 인수를 취할지 알 수 없기 때문이다. 따라서 이렇게 전달받아야 하는 기존 함수의 입력 인수를 알 수 없는 경우에는 *args와 **kwargs 기법을 이용하여 해결해야 한다.
*args, kwargs
*args는 모든 입력 인수를 튜플로 변환하는 매개변수이고 **kwargs는 모든 키-값 형태의 입력 인수를 딕셔너리로 변환하는 매개변수이다. 다음과 같은 형태의 호출을 살펴보자.
>>> func(1, 2, 3, name='foo', age=3)
func() 함수가 입력 인수의 개수와 형태에 상관없이 모든 입력을 처리하려면 어떻게 해야 할까?
>>> def func(*args, **kwargs):
... print(args)
... print(kwargs)
...
>>> func(1, 2, 3, name='foo', age=3)
(1, 2, 3)
{'age': 3, 'name': 'foo'}
이처럼 func() 함수에 *args, **kwargs라는 매개변수를 지정하면 다양한 입력 인수를 모두 처리할 수 있다. 이렇게 하면 1, 2, 3 같은 일반 입력은 args 튜플로 저장하고 name='foo'와 같은 키-값 형태의 입력은 kwargs 딕셔너리로 저장한다.
import time
def elapsed(original_func): # 기존 합수를 인수로 받는다.
def wrapper(*args, **kwargs): # *args, **kwargs 매개변수 추가
start = time.time()
result = original_func(*args, **kwargs) # 전달받은 *args, **kwargs를 입력파라미터로 기존함수 수행
end = time.time()
print("함수 수행시간: %f 초" % (end - start)) # 수행시간을 출력한다.
return result # 함수의 결과를 리턴한다.
return wrapper
@elapsed
def myfunc(msg):
""" 데코레이터 확인 함수 """
print("'%s'을 출력합니다." % msg)
wrapper() 함수의 매개변수로 *args와 **kwargs를 추가하고 기존 함수 실행 시 *args와 **kwargs를 인수로 전달하여 호출하게 했다. 이제 프로그램을 실행하면 오류 없이 다음과 같은 결과를 출력한다.
'You need python'을 출력합니다.
함수 수행시간: 0.000027 초
'KDT > Python' 카테고리의 다른 글
Jupyter Notebook 설치 (1) | 2023.03.14 |
---|---|
변수 타입 어노테이션 (0) | 2023.03.13 |
파일 입출력 (1) | 2023.03.13 |
예외 처리 (0) | 2023.03.13 |
모듈 (0) | 2023.03.10 |