소개
이 랩에서는 데코레이터 (decorator) 가 무엇이며 Python 에서 어떻게 작동하는지 배우게 됩니다. 데코레이터는 소스 코드를 변경하지 않고도 함수 동작을 수정할 수 있게 해주는 강력한 기능이며, Python 프레임워크와 라이브러리에서 널리 사용됩니다.
또한 간단한 로깅 데코레이터를 만들고 함수 유효성 검사를 위한 더 복잡한 데코레이터를 구현하는 방법을 배우게 됩니다. 이 랩에 포함된 파일은 logcall.py, sample.py, 그리고 수정될 validate.py입니다.
첫 번째 데코레이터 만들기
데코레이터란 무엇인가요?
Python 에서 데코레이터는 초보자에게 매우 유용할 수 있는 특별한 구문입니다. 데코레이터를 사용하면 함수 또는 메서드의 동작을 수정할 수 있습니다. 데코레이터를 다른 함수를 입력으로 받는 함수라고 생각하십시오. 그런 다음 새로운 함수를 반환합니다. 이 새로운 함수는 종종 원래 함수의 동작을 확장하거나 변경합니다.
데코레이터는 @ 기호를 사용하여 적용됩니다. 이 기호 뒤에 데코레이터 이름을 함수 정의 바로 위에 배치합니다. 이것은 특정 함수에 데코레이터를 사용하려는 것을 Python 에 알리는 간단한 방법입니다.
간단한 로깅 데코레이터 만들기
함수가 호출될 때 정보를 기록하는 간단한 데코레이터를 만들어 보겠습니다. 로깅은 실제 응용 프로그램에서 일반적인 작업이며, 이를 위해 데코레이터를 사용하는 것은 데코레이터가 어떻게 작동하는지 이해하는 좋은 방법입니다.
먼저 VSCode 편집기를 엽니다.
/home/labex/project디렉토리에서logcall.py라는 새 파일을 만듭니다. 이 파일은 데코레이터 함수를 저장합니다.logcall.py에 다음 코드를 추가합니다.
## logcall.py
def logged(func):
print('Adding logging to', func.__name__)
def wrapper(*args, **kwargs):
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
이 코드가 무엇을 하는지 자세히 살펴보겠습니다.
logged함수는 데코레이터입니다. 다른 함수 (func라고 부름) 를 인수로 받습니다. 이func는 로깅을 추가하려는 함수입니다.- 데코레이터가 함수에 적용되면 메시지를 출력합니다. 이 메시지는 지정된 이름의 함수에 로깅이 추가되고 있음을 알려줍니다.
logged함수 내부에서wrapper라는 내부 함수를 정의합니다. 이wrapper함수가 원래 함수를 대체합니다.- 데코레이팅된 함수가 호출되면
wrapper함수는 해당 함수가 호출되고 있음을 알리는 메시지를 출력합니다. - 그런 다음 원래 함수 (
func) 를 해당 함수에 전달된 모든 인수로 호출합니다.*args및**kwargs는 임의의 수의 위치 및 키워드 인수를 허용하는 데 사용됩니다. - 마지막으로 원래 함수의 결과를 반환합니다.
- 데코레이팅된 함수가 호출되면
logged함수는wrapper함수를 반환합니다. 이wrapper함수는 이제 원래 함수 대신 사용되어 로깅 기능을 추가합니다.
데코레이터 사용하기
- 이제 동일한 디렉토리 (
/home/labex/project) 에서 다음 코드를 사용하여sample.py라는 다른 파일을 만듭니다.
## sample.py
from logcall import logged
@logged
def add(x, y):
return x + y
@logged
def sub(x, y):
return x - y
여기서 @logged 구문은 매우 중요합니다. Python 에게 logged 데코레이터를 add 및 sub 함수에 적용하도록 지시합니다. 따라서 이러한 함수가 호출될 때마다 데코레이터에 의해 추가된 로깅 기능이 실행됩니다.
데코레이터 테스트하기
- 데코레이터를 테스트하려면 VSCode 에서 터미널을 엽니다. 먼저 다음 명령을 사용하여 디렉토리를 프로젝트 디렉토리로 변경합니다.
cd /home/labex/project
그런 다음 Python 인터프리터를 시작합니다.
python3
- Python 인터프리터에서
sample모듈을 가져와 데코레이팅된 함수를 테스트합니다.
>>> import sample
Adding logging to add
Adding logging to sub
>>> sample.add(3, 4)
Calling add
7
>>> sample.sub(2, 3)
Calling sub
-1
>>> exit()
sample 모듈을 가져올 때 "Adding logging to..." 메시지가 출력되는 것을 확인하십시오. 이는 모듈을 가져올 때 데코레이터가 적용되기 때문입니다. 데코레이팅된 함수 중 하나를 호출할 때마다 "Calling..." 메시지가 출력됩니다. 이는 데코레이터가 예상대로 작동하고 있음을 보여줍니다.
이 간단한 데코레이터는 데코레이터의 기본 개념을 보여줍니다. 원래 함수의 코드를 변경하지 않고 추가 기능 (이 경우 로깅) 으로 원래 함수를 래핑합니다. 이것은 Python 에서 다양한 시나리오에서 사용할 수 있는 강력한 기능입니다.
유효성 검사 데코레이터 구축하기
이 단계에서는 더 실용적인 데코레이터를 만들 것입니다. Python 의 데코레이터는 다른 함수의 동작을 수정할 수 있는 특수한 유형의 함수입니다. 우리가 만들 데코레이터는 타입 어노테이션 (type annotation) 을 기반으로 함수 인수를 검증합니다. 타입 어노테이션은 함수의 인수와 반환 값에 대해 예상되는 데이터 유형을 지정하는 방법입니다. 이는 함수가 올바른 입력 유형을 수신하도록 보장하여 많은 버그를 방지하는 데 도움이 되므로 실제 응용 프로그램에서 일반적인 사용 사례입니다.
유효성 검사 클래스 이해하기
이미 validate.py라는 파일을 만들었으며, 여기에는 몇 가지 유효성 검사 클래스가 포함되어 있습니다. 유효성 검사 클래스는 값이 특정 기준을 충족하는지 확인하는 데 사용됩니다. 이 파일의 내용을 보려면 VSCode 편집기에서 열어야 합니다. 터미널에서 다음 명령을 실행하여 이 작업을 수행할 수 있습니다.
cd /home/labex/project
code validate.py
이 파일에는 세 개의 클래스가 있습니다.
Validator- 이것은 기본 클래스입니다. 기본 클래스는 다른 클래스가 상속할 수 있는 일반적인 프레임워크 또는 구조를 제공합니다. 이 경우 유효성 검사를 위한 기본 구조를 제공합니다.Integer- 이 유효성 검사기 클래스는 값이 정수인지 확인하는 데 사용됩니다. 이 유효성 검사기를 사용하는 함수에 정수가 아닌 값을 전달하면 오류가 발생합니다.PositiveInteger- 이 유효성 검사기 클래스는 값이 양의 정수인지 확인합니다. 따라서 음의 정수 또는 0 을 전달하면 오류가 발생합니다.
유효성 검사 데코레이터 추가하기
이제 validate.py 파일에 validated라는 데코레이터 함수를 추가할 것입니다. 이 데코레이터는 몇 가지 중요한 작업을 수행합니다.
- 함수의 타입 어노테이션을 검사합니다. 타입 어노테이션은 함수가 어떤 종류의 데이터를 예상하는지 알려주는 작은 메모와 같습니다.
- 이러한 타입 어노테이션에 대해 함수에 전달된 인수를 검증합니다. 즉, 함수에 전달된 값이 올바른 유형인지 확인합니다.
- 또한 함수의 반환 값을 해당 어노테이션에 대해 검증합니다. 따라서 함수가 반환해야 하는 데이터 유형을 반환하는지 확인합니다.
- 유효성 검사가 실패하면 유용한 오류 메시지를 발생시킵니다. 이러한 메시지는 어떤 인수에 잘못된 유형이 있는지와 같이 정확히 무엇이 잘못되었는지 알려줍니다.
validate.py 파일의 끝에 다음 코드를 추가합니다.
## Add to validate.py
import inspect
import functools
def validated(func):
sig = inspect.signature(func)
print(f'Validating {func.__name__} {sig}')
@functools.wraps(func)
def wrapper(*args, **kwargs):
## Bind arguments to the signature
bound = sig.bind(*args, **kwargs)
errors = []
## Validate each argument
for name, value in bound.arguments.items():
if name in sig.parameters:
param = sig.parameters[name]
if param.annotation != inspect.Parameter.empty:
try:
## Create an instance of the validator and validate the value
if isinstance(param.annotation, type) and issubclass(param.annotation, Validator):
validator = param.annotation()
bound.arguments[name] = validator.validate(value)
except Exception as e:
errors.append(f' {name}: {e}')
## If validation errors, raise an exception
if errors:
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
## Call the function
result = func(*bound.args, **bound.kwargs)
## Validate the return value
if sig.return_annotation != inspect.Signature.empty:
try:
if isinstance(sig.return_annotation, type) and issubclass(sig.return_annotation, Validator):
validator = sig.return_annotation()
result = validator.validate(result)
except Exception as e:
raise TypeError(f'Bad return: {e}') from None
return result
return wrapper
이 코드는 Python 의 inspect 모듈을 사용합니다. inspect 모듈을 사용하면 함수와 같은 라이브 객체에 대한 정보를 얻을 수 있습니다. 여기서는 함수의 시그니처를 검사하고 타입 어노테이션을 기반으로 인수를 검증하는 데 사용합니다. 또한 functools.wraps를 사용합니다. 이것은 이름과 docstring 과 같은 원래 함수의 메타데이터를 보존하는 도우미 함수입니다. 메타데이터는 함수가 수행하는 작업을 이해하는 데 도움이 되는 함수에 대한 추가 정보와 같습니다.
유효성 검사 데코레이터 테스트하기
유효성 검사 데코레이터를 테스트할 파일을 만들어 보겠습니다. test_validate.py라는 새 파일을 만들고 다음 코드를 추가합니다.
## test_validate.py
from validate import Integer, PositiveInteger, validated
@validated
def add(x: Integer, y: Integer) -> Integer:
return x + y
@validated
def pow(x: Integer, y: Integer) -> Integer:
return x ** y
## Test with a class
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def cost(self):
return self.shares * self.price
@validated
def sell(self, nshares: PositiveInteger):
self.shares -= nshares
이제 Python 인터프리터에서 데코레이터를 테스트해 보겠습니다. 먼저 프로젝트 디렉토리로 이동하여 터미널에서 다음 명령을 실행하여 Python 인터프리터를 시작합니다.
cd /home/labex/project
python3
그런 다음 Python 인터프리터에서 다음 코드를 실행하여 데코레이터를 테스트할 수 있습니다.
>>> from test_validate import add, pow, Stock
Validating add (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating pow (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating sell (self, nshares: validate.PositiveInteger) -> <class 'inspect._empty'>
>>>
>>> ## Test with valid inputs
>>> add(2, 3)
5
>>>
>>> ## Test with invalid inputs
>>> add('2', '3')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/labex/project/validate.py", line 75, in wrapper
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
x: Expected <class 'int'>
y: Expected <class 'int'>
>>>
>>> ## Test valid power
>>> pow(2, 3)
8
>>>
>>> ## Test with negative exponent (produces non - integer result)
>>> pow(2, -1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/labex/project/validate.py", line 83, in wrapper
raise TypeError(f'Bad return: {e}') from None
TypeError: Bad return: Expected <class 'int'>
>>>
>>> ## Test with a class
>>> s = Stock("GOOG", 100, 490.1)
>>> s.sell(50)
>>> s.shares
50
>>>
>>> ## Test with invalid shares
>>> s.sell(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/labex/project/validate.py", line 75, in wrapper
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
nshares: Expected value > 0
>>> exit()
보시다시피 validated 데코레이터는 함수 인수 및 반환 값에 대한 타입 검사를 성공적으로 적용했습니다. 이는 코드를 더 강력하게 만들기 때문에 매우 유용합니다. 타입 오류가 코드 깊숙이 전파되어 찾기 어려운 버그를 일으키도록 하는 대신, 함수 경계에서 이를 포착합니다.
요약
이 랩에서는 Python 의 데코레이터에 대해 배웠습니다. 데코레이터가 무엇인지, 어떻게 작동하는지 등을 포함합니다. 또한 함수에 동작을 추가하기 위한 간단한 로깅 데코레이터를 만드는 방법을 익혔고, 타입 어노테이션을 기반으로 함수 인수를 검증하기 위한 더 복잡한 데코레이터를 구축했습니다. 또한 함수 시그니처를 분석하기 위해 inspect 모듈을 사용하고 함수 메타데이터를 보존하기 위해 functools.wraps를 사용하는 방법을 배웠습니다.
데코레이터는 더 유지 관리 가능하고 재사용 가능한 코드를 작성할 수 있게 해주는 강력한 Python 기능입니다. 로깅, 접근 제어 및 캐싱과 같은 횡단 관심사 (cross-cutting concerns) 에 대해 Python 프레임워크 및 라이브러리에서 일반적으로 사용됩니다. 이제 이러한 기술을 자체 Python 프로젝트에 적용하여 더 깔끔하고 유지 관리 가능한 코드를 만들 수 있습니다.