본문 바로가기

Computer/Python

Decorator 에서 함수 디폴트 인자 파악 방법

반응형

데코레이터는 자신이 감싸고 있는 함수가 호출되기 전과 후에 코드를 추가로 실행하는 파이썬의 문법으로 여러 함수에 대해 동일한 기능을 수행시키고 싶을 때 주로 사용합니다. 다음 코드와 같이 장식할 함수를 인자로 받아 wrapper 함수에서 인자로 받은 함수를 수행하고 wrapper 함수가 반환되므로 sample 함수 호출 시 전달한 인자는 wrapper 함수의 인자 *args, **kwargs가 됩니다.

def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f'{func.__name__}')
        print(f'Arguments: {args!r}, Keywords: {kwargs!r}', end = '')
        return result
    return wrapper
    
@trace
def sample(a, b=10, *args, c=None, **kwargs):
    pass

sample(1, 2, c=20, d=30)

wrapper 함수에 전달된 args, kwargs를 살펴보면 다음과 같이 명시적으로 전달된 인자들만 나오고 sample 함수에서 선언된 디폴트 인자들은 검출되지 않습니다.


데코레이터 사용시 함수의 디폴트 인자를 파악하려면 inspect 내장 라이브러리의 signature 모듈을 사용하면 됩니다. signature에 함수를 전달하면 parameters 프로퍼티를 통해 함수 아규먼트의 이름, 디폴트 값, 인자 종류를 파악할 수 있고 parameters 프로퍼티는 아규먼트 이름에 따른 OrderedDict 타입으로 구성되어 있습니다.

from inspect import signature
def sample(a, b=10, *args, c=None, **kwargs):
    pass

sig = signature(sample)

for p in sig.parameters.values():
    print(f'Argument Name: {p.name}, Default value: {p.default}')

위의 sample 함수의 경우 a, args, kwargs 인자에는 디폴트 값이 설정되지 않았고 default 속성은 "<class 'inspect._empty'>" 라고 반환합니다. 따라서 디폴트 값이 비었는지 (empty) 확인하려면 inspect.Parameter에 "empty" 프로퍼티가 정의되어 있기 때문에 "p.default is inspect.Parameter.empty" 라는 구문으로 파악할 수 있습니다. 따라서 다음과 같이 디폴트 값이 있는 아규먼트만 딕셔너리를 구성할 수 있고,

import inspect

def get_default_args(func):
    sig = inspect.signature(func)
    return {
        k: v.default
        for k, v in sig.parameters.items()
        if v.default is not inspect.Parameter.empty
    }

또한, parameter의 kind 프로퍼티를 이용하면 아규먼트 타입을 알 수 있음을 이용하여 'POSITIONAL'로 필터링해 키워드 형식이 아닌 위치에 따른 인자 전달도 추적할 수 있습니다. 이를 통해 다시 trace 데코레이터를 재구성하면 다음과 같습니다.

def get_passed_args(func):
    sig = inspect.signature(func)
    args_list = []
    for param in sig.parameters.values():
        if 'POSITIONAL' in str(param.kind):
            args_list.append(param.name)
    return args_list

def trace(func):
    def wrapper(*args, **kwargs):
        kwds = get_default_args(func)
        args_list = get_passed_args(func)
        num_args = len(args_list) - 1 if 'args' in args_list else len(args_list)
        args_dict = {arg_name: arg_value for arg_name, arg_value in zip(args_list[:num_args], args[:num_args])}
        if 'args' in args_list:
            args_dict['args'] = args[num_args:]
        print(f'Passed Args {args_dict!r}')
        print(f'Passed Keywords {kwargs!r}')
        print(f'Default keywords: {kwds!r}')
        result = func(*args, **kwargs)
        return result
    return wrapper
    
@trace
def sample(a, b=10, *args, c=None, **kwargs):
    print(a, b, args, c, kwargs)

sample(1, 2, 3, 4, c=20, d=30)

반응형

'Computer > Python' 카테고리의 다른 글

List Subtraction  (0) 2021.07.26
파이썬의 GIL 사용 이유  (2) 2021.07.24
namedtuple 인스턴스 확인  (0) 2021.07.19
파이썬의 memory management  (3) 2021.07.14
파이썬과 객체  (0) 2021.07.14