파이썬의 변수 범위에 대해 알아 볼게요.
# 예제1
def func_v1(a):
print(a)
print(b)
# 예외
# func_v1(5)
b = 10
# 예제 2
def func_v2(a):
print(a)
print(b)
func_v2(5)
# 예제 3
b = 10
def func_v3(a):
print(a)
print(b)
b = 5
func_v2(5) # 값이 할당되기 이전에 출력하려고 하니 error발생
예제1, 예제2의 경우 무난히 알 수 있지만 예제3은 왜? 전역변수가 참조되지 않을까요. 이미 로컬변수 b가 있을때는 전역은 참조하지 않아요.
같은 변수가 있을때 [전역변수 < 지역변수] 참조 순위는 이렇게 결정됨
이를 증명하기 위해 dis 패키지를 사용해볼게요.
함수를 정의한 후 어떻게 바이트코드(파이썬 인터프리터 엔진이 바이트로 해석한)를 우리가 짠 함수의 흐름으로 보여 질 수 있는지 옅보게됨.
b = 10
def func_v3(a):
print(a)
print(b)
b=1
# func_v3(5)
from dis import dis
print('ex1-1')
print(dis(func_v3))
output
ex1-1
35 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP
36 8 LOAD_GLOBAL 0 (print)
10 LOAD_FAST 1 (b)
12 CALL_FUNCTION 1
14 POP_TOP
37 16 LOAD_CONST 1 (1)
18 STORE_FAST 1 (b)
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
바로 위에서 설명한 개념을 이해했다면 이제 clouser를 이해하는 첫 단추를 꾈 수 있게 되요.
클로저 : 반환되는 내부 함수에 대해서 선언된 연결을 가지고 참조하는 방식을 의미
- 반환 당시 함수 유효 범위를 벗어나 변수 또는 메소드에 직접 접근이 가능
a = 10
print('ex2-1', a+10)
print('ex2-2', a+100)
# 결과를 누적 할 수 없을까?
print('ex2-3', sum(range(1,51)))
print('ex2-4', sum(range(51,100)))
output
ex2-1 20
ex2-2 110
ex2-3 1275
ex2-4 3675
위 예제를 통해서 sum()메서드와 같은 누적하여 더하는 그런 객체를 만들 수 없을까? 라는 의문점을 갖을 수 있음.(아님 말궁)
# class 이용
class Averager():
def __init__(self):
self._series = []
def __call__(self, v):
self._series.append(v)
print('class >>> {} / {}'.format(self._series, len(self._series)))
return sum(self._series) / len(self._series)
# 인스턴스 생성
avg_cls = Averager()
# 누적확인
print('ex3-1', avg_cls(15))
print('ex3-1', avg_cls(35))
print('ex3-1', avg_cls(40))
# 결국 sum함수도 위와 같은 비슷한 원리가 아닐가라는 생각이듬.
# 여기까지 우리는 클로저를 배우기 위해 밑밥을 깐거에요.
output
class >>> [15] / 1
ex3-1 15.0
class >>> [15, 35] / 2
ex3-1 25.0
class >>> [15, 35, 40] / 3
ex3-1 30.0
평균을 구할수 있는 Averager라는 클래스를 만들어 볼게요.
내부에는 __init__, __call__을 선언하도록 할거에요.
메서드이름()
하면 실행되게 하기 위해서에요. 전역변수 사용 감소
디자인 패턴 적용
def closure_avg1():
series = []
# <-- --> 파이썬에서는 함수와 함수 사이의 공간을
# Free variable이라고 지칭함 == closure scope
def averager(v):
series.append(v)
print('class >>> {} / {}'.format(series, len(series)))
return sum(series) / len(series)
return averager
avg_closure1 = closure_avg1()
print('ex4-1', avg_closure1(15))
print('ex4-2', avg_closure1(35))
print('ex4-3', avg_closure1(40))
output
class >>> [15] / 1
ex4-1 15.0
class >>> [15, 35] / 2
ex4-2 25.0
class >>> [15, 35, 40] / 3
ex4-3 30.0
만약 averager메서드 내부에 빈 series 리스트를 선언하면 값들이 누적되어 저장되지 않아요.
활용 범위
마우스를 클릭 누적횟수, 로그 기록들을 누적할때, 변수를 계속 살리는 기능에 많이 활용되요.
이제는 클로저 뜯어보기를 해볼게요.
print('ex5-1', dir(avg_closure1))
print()
print('ex5-2', dir(avg_closure1.__code__))
print()
print('ex5-3', avg_closure1.__code__.co_freevars)
print()
print('ex5-4', dir(avg_closure1.__closure__[0]))
print()
print('ex5-5', avg_closure1.__closure__[0].cell_contents)
output
ex5-1 ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
ex5-2 ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_posonlyargcount', 'co_stacksize', 'co_varnames', 'replace']
ex5-3 ('series',)
ex5-4 ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
ex5-5 [15, 35, 40]
avg_closure1.__code__.co_freevars
이걸 차분히 보면 우선 코드 객체에서 co_freevars를 불렀는데요. 그러니 ('series',)
라는 튜플이 나오네요.cell_contents
메소드를 호출하니 그동안 인자값으로 넣고 리스트에 저장된 값들이 출력되네요. # 잘못된 클로저 사용 예시
def closure_avg2():
# Free variable
cnt = 0
total = 0
# 클로저 영역
def averager(v):
# nonlocal cnt, total # nonlocal로 선언한 이후면 가능함
cnt = cnt + 1
total = total + v
print('def2 >>> {} / {}'.format(total, (cnt)))
return total / cnt
return averager
avg_closure2 = closure_avg2()
print('ex5-5 - ', avg_closure2(15))
print('ex5-6 - ', avg_closure2(35))
print('ex5-7 - ', avg_closure2(40))
output
ex5-5 - 15.0
def2 >>> 50 / 2
ex5-6 - 25.0
def2 >>> 90 / 3
ex5-7 - 30.0
closure_avg2 메서드의 로컬 변수 cnt, total와 averager 메서드의 cnt, total은 별개임.
만약 closure_avg2 메서드의 로컬 변수 cnt, total을 사용하고자 한다면 nonlocal cnt, total
을 정의하면 할당전 참조 오류가 오류가 아래와 같이 발생하지 않음
UnboundLocalError: local variable 'cnt' referenced before assignment
이 글을 쓰는데도 데코레이터가 100% 이해는 되지 않아요. 하지만 요점은
메서드 이름을 매개변수로 사용하여 메서드 scope내에서 호출하여
반환된 값을 변수에 저장하여 활용한다는 점이 Key Point!
실습 모델로 perf_clock메서드와 그 매개변수로 func라고 지으며 다시 메서드 안에 perf_clocked 메서드를 만들고 매개변수를 *args로 만듭니다.
이후 time.perf_counter()메서드를 이용해 처음 시간을 변수 st에 저장하고 perf_clock 메서드의 매개변수 func를 이용해 함수 호출을 합니다. 이때 매개변수는 perf_clocked의 매개변수 *args를 그대로 사용해 매개변수로 둡니다. et 변수에는 끝나는 시간을 저장합니다.
이후 result라는 변수로 return받은 값을 저장합니다.
기존 perf_clock메서드의 영역에서 나와 time_func, sum_func, fact_func를 만들어요.
import time
def perf_clock(func):
def perf_clocked(*args):
# 시작 시간
st = time.perf_counter()
result = func(*args)
# 종료 시간
et = time.perf_counter() - st
# 함수명
name = func.__name__
# 매개변수
arg_str = ','.join(repr(arg) for arg in args)
# 출력
print('Result : [%0.5fs] %s(%s) -> %r' %(et, name, arg_str,result))
return result
return perf_clocked
def time_func(seconds):
time.sleep(seconds)
def sum_func(*numbers):
return sum(numbers)
def fact_func(n):
return 1 if n < 2 else n * fact_func(n-1)
데코레이터를 사용하지 않은 방식으로 구현하려면 코드의 가독성이 떨어진다는 단점이 있습니다. 하지만 차근차근 볼게요.
이어서 아래에 각각 앞서 구현한 세개의 함수를 perf_clock()의 매개변수로 사용하기 위해 인자로 넣습니다. 그리고 non_deco1~3으로 생성하고요.
non_deco1 = perf_clock(time_func)
non_deco2 = perf_clock(sum_func)
non_deco3 = perf_clock(fact_func)
print('ex7-1-', non_deco1, non_deco1.__code__.co_freevars)
print('ex7-2-', non_deco2, non_deco2.__code__.co_freevars)
print('ex7-3-', non_deco3, non_deco3.__code__.co_freevars)
print('*'*40, 'called non deco -> time_func')
print('ex7-4')
non_deco1(2)
print('*'*40, 'called non deco -> sum_func')
print('ex7-5')
non_deco2(100,200,300,500)
print('*'*40, 'called non deco -> fact_func')
print('ex7-6')
non_deco3(100)
output
ex7-1- <function perf_clock.<locals>.perf_clocked at 0x0000020BF62F29D0> ('func',)
ex7-2- <function perf_clock.<locals>.perf_clocked at 0x0000020BF62F2A60> ('func',)
ex7-3- <function perf_clock.<locals>.perf_clocked at 0x0000020BF62F2AF0> ('func',)
**************************************** called non deco -> time_func
ex7-4
Result : [2.00062s] time_func(2) -> None
**************************************** called non deco -> sum_func
ex7-5
Result : [0.00002s] sum_func(100,200,300,500) -> 1100
**************************************** called non deco -> fact_func
ex7-6
Result : [0.00095s] fact_func(100) -> 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
ex7-1을 호출하면 function 객체가 생성되고 해당 이름이 처음 perf_clock에 있고 local 영역에 perf_clocked라는 메서드가 추가로 있음을 확인할 수 있어요.
ex7-2, ex7-3 역시 메모리의 주소값만 달라지고 나머지는 동일해요.
ex7-4의 경우에는 non_deco1을 호출할 경우 perf_clock(time_func)으로 만든어진 메소드 로직이 작동하여 2초라는 결과 값이 나와요.
ex7-5도 비슷한 방식으로 장독하지만 sum()로직이 내부에서 작동하고 인자값으로 100, 200, 300, 500을 합산하고 그 시간이 0.00002초라고 찍히게되요.
ex7-6의 경우에는 100! factorial을 연산하여 엄청난 결과값이 나오게 됩니다. 하지만 0.00095초가 걸리는걸 보면 그래도 빨리 느껴지네요.
기존 이중 메서드 구조를 사용한 것을 공통적으로 두고 아래에 소스코드가 기존과 다른 모습을 보이게 됩니다. time_func, sum_func, fact_func 머리위에 @perf_clock
라고 소스코드를 3개의 메소드명 위에 작성하게되요. 이렇게 작성하는 것이 기존 메서드안에 매개변수로 넣어서 상속하는것과 동일한 결과를 낳게 됩니다.
그럼 직관적으로 시간과 관련된 메서드를 사용하고 싶으면 바로 time_func를 호출하여 사용하면 됩니다.
데코레이터 미사용 때는 어떻게 했조? perf_clock(time_func)메서드 안에 내가 사용하고 싶은 메서드를 인자값으로 두고 이를 변수로 재생성하여 원하는 인자값을 넣고 호출하였기에 몇차례 꼬았었조?
@perf_clock
def time_func(seconds):
time.sleep(seconds)
@perf_clock
def sum_func(*numbers):
return sum(numbers)
@perf_clock
def fact_func(n):
return 1 if n < 2 else n * fact_func(n-1)
print('*'*40, 'called non deco -> time_func')
print('ex7-7')
time_func(2)
print('*'*40, 'called non deco -> sum_func')
print('ex7-8')
sum_func(10,20,30,40,50)
print('*'*40, 'called non deco -> fact_func')
print('ex7-9')
fact_func(100)
output
**************************************** called non deco -> time_func
ex7-7
Result : [1.99995s] time_func(2) -> None
**************************************** called non deco -> sum_func
ex7-8
Result : [0.00001s] sum_func(10,20,30,40,50) -> 150
**************************************** called non deco -> fact_func
ex7-9
Result : 해당값은 상기 소스코드 ex7-6의 결과와 동일함
output
output
output