데코레이터라는 명칭의 모호성. 사실 데코레이터는 구문 트리를 파싱하고 애너테이션하는 컴파일러 분야에서의 용법과 관련이 더 깊다.
함수 데코레이터는 소스 코드에 있는 함수를 '표시'해서 함수의 작동을 개선할 수 있게 해준다. 데코레이터를 자유자재로 사용하려면 먼저 클로저를 알아야 한다.
자신만의 데코레이터를 구현하고자 한다면 클로저를 잘 이해해야 하며, nonlocal
이 필요해진다.
클로저는 콜백을 이용한 효율적인 비동기 프로그래밍과 필요에 따라 함수형 스타일로 코딩하는 데에도 필수적이다.
목표
- 단순한 등록 데코레이터
- 복잡한 매개변수화된 데코레이터
- 함수 데코레이터가 정확히 어떻게 동작하는가 설명할 수 있어야 한다.
선행 지식
- 파이썬이 데코레이터 구문을 평가하는 방식
- 변수가 지역 변수인지 파이썬이 판단하는 방식
- 클로저의 존재 이유와 작동 방식
- nonlocal로 해결할 수 있는 문제
선행 지식을 갖춘 후 다룰 수 있는 주제
- 잘 작동하는 데코레이터 구현
- 표준 라이브러리에서 제공하는 데코레이터들
- 매개변수화된 데코레이터 구현
7.1 기본 지식
데코레이터는 다른 함수를 인수로 받는 콜러블(데코레이트된 함수)이다. 데코레이터는 데코레이트된 함수에 어떤 처리를 수행하고 함수를 반환하거나 함수를 다른 함수나 콜러블 객체로 대체한다.
@decorate
def target():
print('hi()')
def hi():
print('hi()')
hi = decorate(hi)
def deco(func):
def inner():
print('running inner()')
return inner
@deco
def target():
print('running target()')
target()
print(target)
# result
# running inner()
# <function deco.<locals>.inner at 0x105c7aa70>
deco()는 inner() 함수 객체를 반환한다.
deco로 데코레이트된 target() 함수를 호출하면 deco()가 실행된다.
target은 inner()를 가리키고 있다.
요약
- 데코레이터는 데코레이트된 함수를 다른 함수로 대체하는 능력이 있다.
- 데코레이터는 로딩될 때 바로 실행된다.
7.2 파이썬이 데코레이터를 실행하는 시점
데코레이터의 핵심 특징은 데코레이트된 함수가 정의된 직후에 실행된다는 것이다. 파이썬이 모듈을 로딩하는 시점, import time에 실행된다.
데코레이터 함수들은 main()보다 먼저 실행된다.
함수 데코레이터는 모듈이 임포트되자마자 실행되지만, 데코레이트된 함수는 명시적으로 호출될 때만 실행된다.
import time, run time
7.3 데코레이터로 개선한 전략 패턴
리스트에 함수를 담아서 함수를 매번 실행하는 로직이 있는 경우, 추가되는 함수를 리스트에 추가하는 것을 잊었다면 문제가 생긴다. 호출해야 하는 각각의 함수에 리스트에 함수를 추가하는 데코레이터로 데코레이트를 해준다.
장점
- 함수명이 특별한 형태일 필요가 없다. 메서드 명에 _calc 등 의미를 추가할 필요가 없다
- 데코레이터가 데코레이트된 함수의 목적을 알려준다.
- 데코레이터가 있는 한 함수를 어느 모듈에서든 적용할 수 있다.
대부분의 데코레이터는 데코레이트된 함수를 변경한다. 즉, 내부 함수를 정의하고 그것을 반환하여 데코레이트된 함수를 대체한다. 내부 함수를 사용하는 코드는 제대로 작동하기 위해 거의 항상 클로저에 의존한다. 클로저를 이해하기 위해 파이썬에서 변수 범위의 작동 방식에 대해 살펴보자.
7.4 변수 범위 규칙
b = 6
def f2(a):
print(a)
print(b)
b = 9
f2(3)
# result
'''
3
Traceback (most recent call last):
File "/Users/simon/Documents/python, flask study/variable scope rule.py", line 7, in <module>
f2(3)
File "/Users/simon/Documents/python, flask study/variable scope rule.py", line 4, in f2
print(b)
UnboundLocalError: local variable 'b' referenced before assignment
'''
f2()가 호출되었을 때 파이썬이 함수를 컴파일하는데 b를 지역 변수로 판단한다. b는 할당되기 이전에 사용되므로 에러가 난다. 인터프리터에게 전역 변수를 사용한다고 알리려면 함수 내에 global 키워드를 붙여서 사용해야 한다.
함수 내 변수는 일단 지역 변수로 인식함.
7.5 클로저
익명 함수와 클로저는 다르다. 익명 함수를 이용하면서 함수 안에 함수를 정의하는 방식이 보편화되었기 때문으로 생각된다. 클로저는 내포된 함수 안에서만 의미가 있다.
클로저는 함수 본체에서 정의하지 않고 참조하는 비전역 변수를 포함한 확장 범위를 가진 함수다. 함수가 익명인지 여부는 상관없다. 함수 본체 외부에 정의된 비전역 변수에 접근할 수 있다는 것이 중요하다.
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = make_averager()
print(avg(10))
print(avg(11))
print(avg(12))
print(avg.__code__.co_varnames)
# ('new_value', 'total')
print(avg.__code__.co_freevars)
# ('series',)
print(avg.__closure__)
# (<cell at 0x10feeead0: list object at 0x10dbcdaa0>,)
print(avg.__closure__[0].cell_contents)
# [10, 11, 12]
avg 함수는 어디서 series를 찾을까?
series는 make_averager() 함수의 지역 변수다. avg(10)을 호출할 때 series 지역 변수는 이미 사라져 버린 후다.
averager() 안에 있는 series는 자유 변수다. 자유 변수는 지역 변수에 바인딩되어 있지 않은 변수다.
series에 대한 바인딩은 반환된 avg() 함수의 __closure__
속성에 저장된다. avg.__closure__
의 각 항목은 avg.__code__.co_freevars
의 이름에 대응된다. 이 항목은 cell 객체며, 이 객체의 cell_contents 속성에서 실제 값을 찾을 수 있다.
클로저는 함수를 정의할 때 존재하던 자유 변수에 대한 바인딩을 유지하는 함수다. 함수를 정의하는 범위가 사라진 후에 함수를 호출해도 자유 변수에 접근할 수 있다.
함수가 비전역 외부 변수를 다루는 경우는 그 함수가 다른 함수 안에 정의된 경우 뿐이다. 함수에 내포된 함수만 비전역 외부 변수를 다룰 수 있다.
7.6 nonlocal 선언
def make_averager():
count = 0
total = 0
def averager(new_value):
# nonlocal count, total
count += 1
total += new_value
return total/count
return averager
avg = make_averager()
print(avg(10))
print(avg(11))
이 코드의 문제점은 count += 1
에서 발생한다. count = count + 1
과 의미가 같기 때문에 count 변수에 할당할 때 지역 변수로 인식하므로 자유 변수를 사용하지 못하고 값이 유지되지 않는다. 자유 변수를 사용하지 않으므로 더이상 클로저가 아니다. 이를 해결하기 위해 nonlocal을 사용한다.
nonlocal을 사용하면 함수 내에서 변수에 값을 할당하더라도 자유 변수를 나타낸다. 값을 nonlocal 변수에 할당하면 클로저에 저장된 바인딩이 변경된다.
'Python' 카테고리의 다른 글
python static method, class method (0) | 2020.06.08 |
---|---|
파이썬 코드 예제 사이트 (0) | 2020.04.10 |
pip3 사용하기 (0) | 2020.03.08 |
파이썬으로 기상청 API 호출하기 동네예보정보조회서비스 (0) | 2019.07.23 |
파이썬 질문 (0) | 2019.07.21 |