내부 함수가 자신을 감싸고 있는 외부 함수의 변수에 접근할 수 있는 특성을 가진다.
외부 함수가 호출된 후에도 그 변수의 상태를 기억하고 사용할 수 있는 기능이다.
주로 데이터 은닉과 상태 유지에 활용된다.
def mul2(n):
return n * 2
print(mul2(10))
print(mul2(5))
20
10
def mul5(n):
return n * 5
print(mul5(10))
print(mul5(5))
50
25
위에 코드를 보면 계속 mul2, mul5를 만들어서 실행하고 있는데,
원하는 숫자를 곱할 때마다 계속 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))
print(mul2.mul(5))
mul5 = Mul(5)
print(mul5.mul(10))
print(mul5.mul(5))
20
10
50
25
위의 클래스 + 일반 메서드 방식의 단점은
메서드 호출이 길다.
→ mul2.mul(10)처럼 .mul()을 매번 붙여야 한다. 간결하지 않음.
함수처럼 쓰기 어렵다.
→ mul2(10)처럼 직접 호출(callable) 할 수 없음. 함수처럼 쓰려면 .mul() 메서드 이름을 반드시 알아야 함.
함수의 개념과 멀다.
→ 곱하기 2라는 간단한 개념을 구현하기 위해 클래스 정의와 인스턴스를 만드는 건 복잡한 구조일 수 있음.
그렇다면 바로 밑의 클래스 + __call__ 오버라이딩 방식을 보자.
class Mul:
def __init__(self, m):
print('생성자 호출')
self.m = m
def __call__(self, n): # 오버라이딩. 객체를 함수처럼 쓸 수 있음.
print('call 호출')
return self.m * n
mul2 = Mul(2)
print(mul2(10))
mul5 = Mul(5)
print(mul5(10))
생성자 호출
call 호출
20
생성자 호출
call 호출
50
위의 클래스 + __call__ 오버라이딩 방식 의 단점은
불필요한 클래스 구조
→ 단순히 곱하기 기능을 만들기 위해 init, call을 갖춘 클래스를 만드는 것은 과함.
메모리 오버헤드
→ 객체를 생성하면 내부적으로 더 많은 메모리/구조가 생기므로 단순 함수형보다 리소스 낭비.
의미 전달이 불명확할 수 있음
→ mul2(10)처럼 함수처럼 동작하지만 사실은 클래스 인스턴스라, 다른 사람이 코드를 볼 때 혼동될 수 있음.
그러면, 간결하고 직관적인 클로저 방식의 코드를 보자.
# 클로저 사용하기
def mul(m):
def wrapper(n):
return m * n
return wrapper
mul2 = mul(2) # 내부 함수를 리턴
print(mul2(10)) # 내부 함수를 이용해서 10을 곱함.
mul5 = mul(5)
print(mul5(10))
20
50
여기서 내부 함수를 리턴한다는 것은,
def wrapper(n):
return m * n
이 내부 함수를 리턴한다는 것.
그래서 만약 mul2 = mul(2) 이런 코드라면
def wrapper(2):
return m * 2
이렇게 됨.
그다음 print(mul2(10)) 이면, 10 * 2 = 20
20 출력!
기존 함수를 수정하거나 확장하는 함수.
다른 함수를 인자로 받아 새로운 함수를 반환하여 원래 함수의 기능을 변경하거나 추가하는 데 사용되며, 주로 코드의 재사용성과 가독성을 높이는 데 활용된다.
import time
def func1(a, b):
start = time.time()
print('함수가 시작되었습니다')
result = a + b
end = time.time()
print(f'함수 수행시간: {end - start}')
return result
result = func1(10, 3)
print(result)
함수가 시작되었습니다
함수 수행시간: 0.001520395278930664
13
함수 수행시간을 더 잘 확인하기 위해 다음 코드를 보자.
print(format(5.984306335449219e-05, ".10f"))
0.0000598431
이 코드를 통해 아주 작은 부동소수점(소수점 위치가 자유롭게 움직일 수 있는 실수 표현 방식) 숫자를 소수점 아래 10자리까지 고정 소수점 형태로 출력한다.
def func2(a, b):
start = time.time()
print('함수가 시작되었습니다')
result = a * b
end = time.time()
print(f'함수 수행시간: {end - start}')
return result
result = func2(10, 3)
print(result)
함수가 시작되었습니다
함수 수행시간: 0.0008776187896728516
30
위 코드들의 단점
이 함수 안에서 직접 타이머와 로그를 작성하고 있어서, 다른 함수에도 동일하게 적용하려면 복붙이 필요함.
유지보수가 어려워짐. 나중에 로깅 방식이 바뀌면 모든 함수에서 수정을 해야 함.
그러면 바로 밑의 데코레이터 방식 코드를 보자.
# 데코레이터 만들기
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):
start = time.time()
print('함수가 시작되었습니다')
result = func(a, b)
end = time.time()
print(f'함수 수행시간: {end - start}')
return result
return wrapper
deco1 = elapsed(func1) # 내부 함수를 리턴
result = deco1(10, 3)
print(result)
함수가 시작되었습니다
함수 수행시간: 0.003779172897338867
13
deco2 = elapsed(func2)
result = deco2(10, 3)
print(result)
함수가 시작되었습니다
함수 수행시간: 0.006906270980834961
30
@elapsed
def func1(a, b):
result = a + b
return result
print(func1(10, 3))
함수가 시작되었습니다
함수 수행시간: 9.942054748535156e-05
13
@elapsed 부분이 데코레이터 문법이다.
@elapsed는 func1 = elapsed(func1)과 같은 의미여서,
func1()이 호출될 때 실제로는 elapsed(func1)이 호출되어 wrapper() 함수가 실행된다.
@elapsed
def func2(a, b):
result = a * b
return result
print(func2(10, 3))
함수가 시작되었습니다
함수 수행시간: 0.0006098747253417969
30