Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python decorator example

I am learning a bit about decorators from a great tutorial on thecodeship but have found myself rather confused by one example.

First a simple example followed by an explanation is given for what a decorator is.

def p_decorate(func):
   def func_wrapper(name):
       return "<p>{0}</p>".format(func(name))
   return func_wrapper

def get_text(name):
   return "lorem ipsum, {0} dolor sit amet".format(name)

my_get_text = p_decorate(get_text)

print my_get_text("John") 

Now this makes sense to me. A decorator is simply a wrapper to a function. And in this guys explanation he says a decorator is a function that takes another function as an argument, generates a new function, and returns the generated function to be used anywhere.

And now the equivalent to the above code is:

def p_decorate(func):
   def func_wrapper(name):
       return "<p>{0}</p>".format(func(name))
   return func_wrapper

@p_decorate
def get_text(name):
   return "lorem ipsum, {0} dolor sit amet".format(name)

print get_text("John")

I believe I understand the way a decorator is initialized when given no arguments. Correct me if I am wrong.

  • The decorator by default passes in the function get_text and because p_decorate returns a function func_wrapper, we end up with the true statement get_text = func_wrapper.

What is important to me is the first code block equivalent, because I see and understand how the decorator is behaving.

What very much confuses me is the following code:

def tags(tag_name):
    def tags_decorator(func):
        def func_wrapper(name):
            return "<{0}>{1}</{0}>".format(tag_name, func(name))
        return func_wrapper
    return tags_decorator

@tags("p")
def get_text(name):
    return "Hello "+name

print get_text("John")

Again, correct me if I'm wrong but this is my understanding.

  • The decorator accepts the tag string "p" instead of the default function name. And in turn the function tags_decorator assumes that the parameter that will be passed in is the function being decorated, get_text.

It might be helpful for me to see the equivalent block of code in "non-decorator" form but I can't seem to wrap my head around what that would begin to look like. I also don't comprehend why tags_decorator and func_wrapper are both returned. What is the purpose of returning two different functions if a decorator only needs to return 1 function to wrap get_text.

As a side note, it really comes down to the following.

  • Can this block be simplified to something less than a set of 3 functions?
  • Can decorators accept more than 1 argument to simplify code?
like image 594
Max Avatar asked Apr 05 '16 13:04

Max


1 Answers

Within limits, everything after the @ is executed to produce a decorator. In your first example, what follows after the @ is just a name:

@p_decorate

so Python looks up p_decorate and calls it with the decorated function as an argument:

get_text = p_decorate(get_text)

(oversimplified a bit, get_text is not initially bound to the original function, but you got the gist already).

In your second example, the decorator expression is a little more involved, it includes a call:

@tags("p")

so Python uses tags("p") to find the decorator. In the end this is what then is executed:

get_text = tags("p")(get_text)

The output of tags("p") is the decorator here! I call the tags function itself a decorator factory, it produces a decorator when called. When you call tags(), it returns tags_decorator(). That's the real decorator here.

You could instead remove the decorator function and hardcode the "p" value and use that directly:

def tags_decorator_p(func):
    def func_wrapper(name):
        return "<{0}>{1}</{0}>".format("p", func(name))
    return func_wrapper

@tags_decorator_p
def get_text(name):
    # ...

but then you'd have to create separate decorators for each possible value of the argument to tags(). That's the value of a decorator factory, you get to add parameters to the decorator and alter how your function is decorated.

A decorator factory can take any number of arguments; it is just a function you call to produce a decorator. The decorator itself can only accept one argument, the function-to-decorate.

I said, within limits at the start of my answer for a reason; the syntax for the expression following the @ only allows a dotted name (foo.bar.baz, so attribute access) and a call ((arg1, arg2, keyword=arg3)). See the reference documentation. The original PEP 318 states:

The decorator statement is limited in what it can accept -- arbitrary expressions will not work. Guido preferred this because of a gut feeling [17] .

like image 61
Martijn Pieters Avatar answered Oct 13 '22 02:10

Martijn Pieters