bigbug
bigbug

Reputation: 59464

How to get through decorator to get the underlying function arguments information?

I write get_function_arg_data(func) as below code to get the function func's arguments information:

def get_function_arg_data(func):
    import inspect
    func_data = inspect.getargspec(func)
    args_name = func_data.args #func argument list
    args_default = func_data.defaults #funcargument default data list
    return args_name, args_default


def showduration(user_function):
    ''' show time duration decorator'''
    import time
    def wrapped_f(*args, **kwargs):
        t1 = time.clock()
        result = user_function(*args, **kwargs)
        print "%s()_Time: %0.5f"%(user_function.__name__, time.clock()-t1)
        return result
    return wrapped_f


def foo(para1, para2=5, para3=7):
    for i in range(1000):
        s = para1+para2+para3
    return s

@showduration
def bar(para1, para2, para3):
    for i in range(1000):
        s=para1+para2+para3
    return s

print get_function_arg_data(foo)
bar(1,2,3)
print get_function_arg_data(bar)


>>> 
(['para1', 'para2', 'para3'], (5, 7))
bar()_Time: 0.00012
([], None)
>>> 

get_function_arg_data() works for foo, not for bar for bar is decorated by a decorator @showduration . My question is how to penetrate the decorator to get the underlying function's information (argument list and default value) ?

Thanks for your tips.

Upvotes: 1

Views: 475

Answers (2)

martineau
martineau

Reputation: 123423

I don't think there is, or at least know of, any general way to "penetrate" a decorated function and get at the underlying function's information because Python's concept of function decoration is so general -- if fact, generally speaking, there's nothing that requires or guarantees that the original function will be called at all (although that's usually the case).

Therefore, a more practical question would be: How could I write my own decorators which would allow me to inspect the underlying function's argument information?

One easy way, previously suggested, would be to use Michele Simionato's decorator module (and write decorators compatible with it).

A less robust, but extremely simple way of doing this would be to do what is shown below based on the code in your question:

def get_function_arg_data(func):
    import inspect
    func = getattr(func, '_original_f', func) # use saved original if decorated
    func_data = inspect.getargspec(func)
    args_name = func_data.args #func argument list
    args_default = func_data.defaults #funcargument default data list
    return args_name, args_default

def showduration(user_function):
    '''show time duration decorator'''
    import time
    def wrapped_f(*args, **kwargs):
        t1 = time.clock()
        result = user_function(*args, **kwargs)
        print "%s()_Time: %0.5f"%(user_function.__name__, time.clock()-t1)
        return result
    wrapped_f._original_f = user_function  # save original function
    return wrapped_f

def foo(para1, para2=5, para3=7):
    for i in range(1000):
        s = para1+para2+para3
    return s

@showduration
def bar(para1, para2, para3):
    for i in range(1000):
        s=para1+para2+para3
    return s

print 'get_function_arg_data(foo):', get_function_arg_data(foo)
print 'get_function_arg_data(bar):', get_function_arg_data(bar)

All the modification involves is saving the original function in an attribute named _original_f which is added the wrapped function returned by the decorator. The get_function_arg_data() function then simply checks for this attribute and returns information based its value rather the decorated function passed to it.

While this approach doesn't work with just any decorated function, only ones which have had the special attribute added to them, it is compatible with both Python 2 & 3.

Output produced by the code shown:

get_function_arg_data(foo): (['para1', 'para2', 'para3'], (5, 7))
get_function_arg_data(bar): (['para1', 'para2', 'para3'], None)

Upvotes: 1

martineau
martineau

Reputation: 123423

Assuming you've installed Michele Simionato's decorator module, you can make yourshowdurationdecorator work with it by making some minor modifications to it and to the nestedwrapped_f()function defined in it so the latter fits the signature that module's decorator.decorator() function expects:

import decorator

def showduration(user_function):
    ''' show time duration decorator'''
    import time
    def wrapped_f(user_function, *args, **kwargs):
        t1 = time.clock()
        result = user_function(*args, **kwargs)
        print "%s()_Time: %0.5f"%(user_function.__name__, time.clock()-t1)
        return result
    return decorator.decorator(wrapped_f, user_function)

However, the module really shines because it will let you reduce boilerplate stuff like the above down to just:

import decorator

@decorator.decorator
def showduration(user_function, *args, **kwargs):
    import time
    t1 = time.clock()
    result = user_function(*args, **kwargs)
    print "%s()_Time: %0.5f"%(user_function.__name__, time.clock()-t1)
    return result

With either set of the above changes, your sample code would output:

(['para1', 'para2', 'para3'], (5, 7))
bar()_Time: 0.00026                  
(['para1', 'para2', 'para3'], None)  

Upvotes: 1

Related Questions