1

TLDR

How many functions can I wrap the actual decorator under? By actual decorator I mean the function that accepts the target function the as argument.

When passing arguments to a decorator in Python we do something like the following:

def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):

    print("I make decorators! And I accept arguments: {0}, {1}".format(decorator_arg1, decorator_arg2))

    def my_decorator(func):
        # The ability to pass arguments here is a gift from closures.
        # If you are not comfortable with closures, you can assume it’s ok,
        # or read: https://stackoverflow.com/questions/13857/can-you-explain-closures-as-they-relate-to-python
        print("I am the decorator. Somehow you passed me arguments: {0}, {1}".format(decorator_arg1, decorator_arg2))

        # Don't confuse decorator arguments and function arguments!
        def wrapped(function_arg1, function_arg2) :
            print("I am the wrapper around the decorated function.\n"
                  "I can access all the variables\n"
                  "\t- from the decorator: {0} {1}\n"
                  "\t- from the function call: {2} {3}\n"
                  "Then I can pass them to the decorated function"
                  .format(decorator_arg1, decorator_arg2,
                          function_arg1, function_arg2))
            return func(function_arg1, function_arg2)

        return wrapped

    return my_decorator

This code is taken from this answer.

Notice that there is a wrapper around the actual decorator that handles the arguments that are given to the actual decorator.

The weird part is that you can decorate the target function with wrapper rather than use the decorator like so:

@decorator_maker_with_arguments("Leonard", "Sheldon")
    def decorated_function_with_arguments(function_arg1, function_arg2):
        print("I am the decorated function and only knows about my arguments: {0}"
               " {1}".format(function_arg1, function_arg2))

    decorated_function_with_arguments("Rajesh", "Howard")
#outputs:
#I make decorators! And I accept arguments: Leonard Sheldon
#I am the decorator. Somehow you passed me arguments: Leonard Sheldon
#I am the wrapper around the decorated function. 
#I can access all the variables 
#   - from the decorator: Leonard Sheldon 
#   - from the function call: Rajesh Howard 
#Then I can pass them to the decorated function
#I am the decorated function and only knows about my arguments: Rajesh Howard

So my question is that how many functions can I wrap around the actual decorator?

Take the code below as an example:

def level0(foo):
    print("Level 0")
    def level1(foo):
        print("Level 1")
        def level2(foo):
            print("Level 2")
            def dec(some_func):
                print("Level 3")
                def wrap():
                    print("Foo is " + foo)
                    some_func()
                print("Level 3 End")
                return wrap
            return dec
        return level2
    return level1

@level0("foo")
def test():
    print("From python")

Calling test prints

Level 0
Level 1
TypeError: level2() missing 1 required positional argument: 'foo'

So is there a depth limit of just 2? Or am I doing something wrong?

Let me know if any other details from my side is required.

ng.newbie
  • 2,807
  • 3
  • 23
  • 57

2 Answers2

1
def level0(foo):
    print(type(foo))
    print("Level 0")
    def level1(foo):
        print(type(foo))
        print("Level 1")
        def level2(foo):
            print("Level 2")
            def dec(some_func):
                print("Level 3")
                def wrap():
                    print("Foo is " + foo)
                    some_func()
                print("Level 3 End")
                return wrap
            return dec
        return level2
    return level1

Try this and you will see the difference between "foo" in level0 and level1. Decorators are just syntactic sugar.in your case python will do

test = level0("foo")(test)

however if your code change to this

@level0
def foo():
    print("from foo")

python will do

test = level0(test)
  • Why does it change to function at Level 1? I don't understand. once string then function? – ng.newbie Aug 06 '18 at 11:44
  • I don't understand what you mean. I am not passing a function to level1. – ng.newbie Aug 06 '18 at 11:50
  • @ng.newbie checkout my updated answer, python will simply run the code if you define a decorator – user1856856 Aug 06 '18 at 12:02
  • When you say python will do `level0("foo")(test)` my question is that **why won't Python do something like** `level0("foo")('bar')('baz')(test)`. Why not a **depth of more than 2 is my question**. I hope you get my point. – ng.newbie Aug 06 '18 at 12:02
  • @ng.newbie the foo in def level0(foo): have nothing to do with def level1(foo), you are not calling a level1, you are defining level1,understand???like if you define a function def func(apple):, you don't need to define apple first, right??? – user1856856 Aug 06 '18 at 12:17
1

Decorator "overwrites" the function. It calls a function pointer obtained from a function call that takes as an argument the function. That function pointer is than called with the argument passed to original function.

What you are doing, you are creating a function which returns a decorator and then calling that function, like this:

def this_returns_decorator(arg_passed_to_this_returns_decorator):
        def decorator(function):
                def wrapper(arg_passed_to_test):
                        function(arg_passed_to_this_returns_decorator + "|" + arg_passed_to_test)
                return wrapper
        return decorator

# next line is "equal" to @decorator
# ie. the function this_returns_decorator is called here and returns a decorator
@this_returns_decorator("arg passed to this_returns_decorator") 
def test(arg):
        print(arg)

# decorating is equal to "overwriting the function":
# test = this_returns_decorator("arg passed to this_returns_decorator")(test)

# will print "arg passed to this_returns_decorator|arg_passed_to_test"
test("arg_passed_to_test") 

You can't get more then two levels this way. You can create a function which will return a function which will return a decorator, and so on, but I would interpret that as function nesting, not decorator nesting.

As to the questions:

What is the maximum level I can nest decorators?

I guess this is how you nest decorators:

def w1(f):
   def wrapper(name):
      return "w1 " + f(name)
   return wrapper

def w2(f):
   def wrapper(name):
       return "w2 " + f(name)
   return wrapper

@w2
@w1
def test(name):
        return name

print(test("name"))  # will print "w2 w1 name"

The python language reference only says decorators can be nested, I couldn't find any limit. For fun&testing I have created the following shell script:

#!/bin/bash
set -euo pipefail
tmp=$(mktemp)
trap 'rm $tmp' EXIT
for ((i = 10000;; i*=2)); do

        {
for i in $(seq $i); do
        printf "%s" "
def w$i(f):
        def w():
                f()
        return w
"
done
echo
for i in $(seq $i); do
        printf "%s\n" "@w$i"
done
printf "%s\n" "def test():
        print(\"test\")
print(\"Success\")
"

        } >"$tmp"

        printf "Testing levels i = %d\n" $i
        ( set -x; python3 "$tmp"; )

done

Was able to go up to more than 400000 levels, when my pc started to lag. Probably the levels of decorators are depended on how much memory your system has and how good your python interpreter is.

@edit:

Just discovered that calling a function pointer returned by a function inside a decorator statement is not allowed. However you still can obtain a pointer to the decorator, and then overload the function.

def level0(foo):
    print(type(foo))
    print("Level 0")
    def level1(foo):
        print(type(foo))
        print("Level 1")
        def level2(foo):
            print("Level 2")
            def dec(some_func):
                print("Level 3")
                def wrap():
                    print("Foo is " + foo)
                    some_func()
                print("Level 3 End")
                return wrap
            return dec
        return level2
    return level1

# @level0("1")("2")("3") does not work - SyntaxError
decorator = level0("1")("2")("3")
@decorator
def test():
        print(test)

test()

The decorator regular expression (from python reference) is:

decorator               ::=  "@" dotted_name ["(" [argument_list [","]] ")"] NEWLINE

The ( ) are optional and may be specified only once. Probably we could make a simple workaround to do the calls ourselves:

def workaround(obj, arg):
        if len(arg) == 0:
                return obj
        return workaround(obj(arg[0]), arg[1:])

@workaround(level0, ["1", "2", "3"])
def test2():
        print("test2")

And that's still function nesting.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • Sometimes the code is `@this_returns_decorator` and **sometimes it is** `@this_is_a_decorator`. My question is how many levels can I put before I get a decorator. I might change the subject title to better reflect my question. I hope you understand. – ng.newbie Aug 06 '18 at 12:36
  • The code is `@this_returns_decorator()` or `@this_is_a_decorator`. This is just normal function nesting. You can just `@this_returns_decorator()()()()()()()()()()()()()......` – KamilCuk Aug 06 '18 at 12:38
  • Yes now I understand. Should I edit the question? Or can you add this into the answer? – ng.newbie Aug 07 '18 at 08:38