Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can a decorator pass variables into a function without changing its signature?

Tags:

python

Let me first acknowledge that what I want to do may be considered anything from silly to evil, but I want to find out if I can do it in Python anyway.

Let's say I have a function decorator that takes keyword arguments defining variables, and I want to access those variables in the wrapped function. I might do something like this:

def more_vars(**extras):
    def wrapper(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            return f(extras, *args, **kwargs)
        return wrapped
    return wrapper

Now I can do something like:

@more_vars(a='hello', b='world')
def test(deco_vars, x, y):
    print(deco_vars['a'], deco_vars['b'])
    print(x, y)

test(1, 2)
# Output:
# hello world
# 1 2

The thing I don't like about this is that when you use this decorator, you have to change the call signature of the function, adding the extra variable in addition to slapping on the decorator. Also, if you look at the help for the function, you see an extra variable that you're not expected to use when calling the function:

help(test)
# Output:
# Help on function test in module __main__:
#
# test(deco_vars, x, y)

This makes it look like the user is expected to call the function with 3 parameters, but obviously that won't work. So you'd have to also add a message to the docstring indicating that the first parameter isn't part of the interface, it's just an implementation detail and should be ignored. That's kind of crappy, though. Is there any way to do this without hanging these variables on something in the global scope? Ideally, I'd like it to look like the following:

@more_vars(a='hello', b='world')
def test(x, y):
    print(a, b)
    print(x, y)

test(1, 2)
# Output:
# hello world
# 1 2
help(test)
# Output:
# Help on function test in module __main__:
#
# test(x, y)

I am content with a Python 3 only solution if one exists.

like image 736
user108471 Avatar asked Nov 04 '14 22:11

user108471


People also ask

How do you pass a value dynamically in Python?

Dynamic Function Arguments We simply can make solve_for() accept *args and **kwargs then pass that to func() . Of course, you will need to handle the arguments in the function that will be called.

Can we use decorator inside a function in Python?

Nesting means placing or storing inside the other. Therefore, Nested Decorators means applying more than one decorator inside a function. Python allows us to implement more than one decorator to a function. It makes decorators useful for reusable building blocks as it accumulates the several effects together.


2 Answers

You could do this with some trickery that inserts the variables passed to the decorator into the function's local variables:

import sys
from functools import wraps
from types import FunctionType


def is_python3():
    return sys.version_info >= (3, 0)


def more_vars(**extras):
    def wrapper(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            fn_globals = {}
            fn_globals.update(globals())
            fn_globals.update(extras)
            if is_python3():
                func_code = '__code__'
            else:
                func_code = 'func_code'
            call_fn = FunctionType(getattr(f, func_code), fn_globals)
            return call_fn(*args, **kwargs)
        return wrapped
    return wrapper


@more_vars(a="hello", b="world")
def test(x, y):
    print("locals: {}".format(locals()))
    print("x: {}".format(x))
    print("y: {}".format(y))
    print("a: {}".format(a))
    print("b: {}".format(b))


if __name__ == "__main__":
    test(1, 2)

Can you do this? Sure! Should you do this? Probably not!

(Code available here.)

like image 153
mipadi Avatar answered Oct 08 '22 05:10

mipadi


EDIT: answer edited for readability. Latest answer is on top, original follows.

If I understand well

  • you want the new arguments to be defined as keywords in the @more_vars decorator
  • you want to use them in the decorated function
  • and you want them to be hidden to the normal users (the exposed signature should still be the normal signature)

Have a look at the @with_partial decorator in my library makefun. It provides this functionality out of the box:

from makefun import with_partial

@with_partial(a='hello', b='world')
def test(a, b, x, y):
    """Here is a doc"""
    print(a, b)
    print(x, y)

It yields the expected output and the docstring is modified accordingly:

test(1, 2)
help(test)

yields

hello world
1 2
Help on function test in module <...>:

test(x, y)
    <This function is equivalent to 'test(x, y, a=hello, b=world)', see original 'test' doc below.>
    Here is a doc

To answer the question in your comment, the function creation strategy in makefun is exactly the same than the one in the famous decorator library: compile + exec. No magic here, but decorator has been using this trick for years in real-world applications so it is quite solid. See def _make in the source code.

Note that the makefun library also provides a partial(f, *args, **kwargs) function if you want to create the decorator yourself for some reason (see below for inspiration).


If you wish to do this manually, this is a solution that should work as you expect, it relies on the wraps function provided by makefun, to modify the exposed signature.

from makefun import wraps, remove_signature_parameters

def more_vars(**extras):
    def wrapper(f):
        # (1) capture the signature of the function to wrap and remove the invisible
        func_sig = signature(f)
        new_sig = remove_signature_parameters(func_sig, 'invisible_args')

        # (2) create a wrapper with the new signature
        @wraps(f, new_sig=new_sig)
        def wrapped(*args, **kwargs):
            # inject the invisible args again
            kwargs['invisible_args'] = extras
            return f(*args, **kwargs)

        return wrapped
    return wrapper

You can test that it works:

@more_vars(a='hello', b='world')
def test(x, y, invisible_args):
    a = invisible_args['a']
    b = invisible_args['b']
    print(a, b)
    print(x, y)

test(1, 2)
help(test)

You can even make the decorator definition more compact if you use decopatch to remove the useless level of nesting:

from decopatch import DECORATED
from makefun import wraps, remove_signature_parameters

@function_decorator
def more_vars(f=DECORATED, **extras):
    # (1) capture the signature of the function to wrap and remove the invisible
    func_sig = signature(f)
    new_sig = remove_signature_parameters(func_sig, 'invisible_args')

    # (2) create a wrapper with the new signature
    @wraps(f, new_sig=new_sig)
    def wrapped(*args, **kwargs):
        kwargs['invisible_args'] = extras
        return f(*args, **kwargs)

    return wrapped

Finally, if you rather do not want to depend on any external library, the most pythonic way to do it is to create a function factory (but then you cannot have this as a decorator):

def make_test(a, b, name=None):
    def test(x, y):
        print(a, b)
        print(x, y)
    if name is not None:
        test.__name__ = name
    return test

test = make_test(a='hello', b='world')
test2 = make_test(a='hello', b='there', name='test2')

I'm the author of makefun and decopatch by the way ;)

like image 37
smarie Avatar answered Oct 08 '22 05:10

smarie