[Python]Decorator & Closure

Hyeseong·2020년 12월 4일
0

python

목록 보기
9/22
post-thumbnail

Decorator & Closure

데코레이터 설명

파이썬 변수 범위

여러가지 클로저 구현

클로저 속성

데코레이터 작성 예제

데코레이터 작동 원리

파이썬 변수 범위

파이썬의 변수 범위에 대해 알아 볼게요.


# 예제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

Closure

바로 위에서 설명한 개념을 이해했다면 이제 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__을 선언하도록 할거에요.

  • init의 경우 내부에 리스트 변수인 _series를 만들거에요. 이유는 하나씩 호출할 메서드의 인자값을 리스트에 담기 위해서에요.
  • call의 경우 Averager 클래스의 인스턴스 객체를 callable하게 만들기 위해 뒀는데요. 메서드이름()하면 실행되게 하기 위해서에요.
    • 내부를 확인하면 append()메서드가 리스트 변수에 저장하네요.
    • print문으로 내부 리스트를 보여주고, 몇개가 있는지(len) 수도 보여주네요.
    • return에는 sum()과 len()을 통해서 _series리스트의 합과 개수로 평균을 구하게되요.

전역변수 사용 감소
디자인 패턴 적용

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]
  • 5-1, dir()메소드로 내부를 보니 __closure__, __code__이 잘 보이네요.
  • 5-2, 그리고 __code__객체로 내부의 컴파일된 실행 가능한 바이너리 코드인데요. 그 자체로는 실행은 안되요.
  • 5-3, avg_closure1.__code__.co_freevars 이걸 차분히 보면 우선 코드 객체에서 co_freevars를 불렀는데요. 그러니 ('series',)라는 튜플이 나오네요.
  • 5-4, closure에 뭐가 들어 있는지 [0]인덱스를 불러보면 저렇게 많은 매직 메소드들이 보이조? init, eq, doc,dir, new,reduce도 보이네요.
  • 5-5, 더 내려가서 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


데코레이터 실습

장점

1. 중복제거

2. 클로저 보다 문법 간결

3. 조합해서 사용 용이

단점

1. 디버깅 어려움

2. 에러의 모호함

이 글을 쓰는데도 데코레이터가 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

profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글