A Python decorator, similar with Decorator Pattern, is a specific change to the Python syntax that allows us to more conveniently alter functions and methods. In this post, I will try my best to deep into this advanced feature in Python.

The winning syntax as of now uses the @ symbol

@classmethod
def foo():
    # do something

First-class Function Link to heading

It’s simple to use but difficult to penetrate its fundamental. Before diving, what must be clear is that function is the first-class citizen, which can be dynamically created, destroyed, passed to a function, returned as a value, and have all the rights as other variables in the programming language have.

def say_hello(name):
    return "hello to " + name

# 1. assign function to a variable
say_hello_to = say_hello
print say_hello_to('John') # hello to John

# 2. pass function as parameter to other function
def call_func(func, name):
    return func(name)
print call_func(say_hello, 'John') # hello to John

# 3. return a function in other function
def compose_func():
    def name():
        return 'John'
    return name

print compose_func() # <function name at 0x10bcdccf8>

hello = compose_func()
print hello() # John

'''
4. inner function has access to the enclosing scope
notice how to read a "name" argument from the enclosing scope of the inner function and return the new function.
'''
def compose_func(name):
    def complete_msg():
        return 'hello to ' + name
    return complete_msg

hello = compose_func('John')
print greet() # hello to John

Rough Decorator Implemented by Function Link to heading

The combination of attributes above will help us build a decorator. In this example, let’s simply consider a function that wraps the string output to uppercase.

def get_name(name):
    return name

def p_decorate(func):
    def wrapper(name):
        return name.upper()
    return wrapper

my_name = p_decorate(get_name)
print my_name('John') # JOHN

Python’s Decorator Syntax Link to heading

Python provides a syntactic sugar, a @ symbol, to decorate function easily, replacing my_name = p_decorate(get_name) above.

def p_decorate(func):
    def wrapper(name):
        return name.upper()
    return wrapper

@p_decorate
def get_name(name):
    return name

print my_name('John') # JOHN

Decorating Method Link to heading

Same to function, method can also be wrapped by decorators. Whereas, the first parameter of wrapper function must be reference to the current object.

def p_decorate(func):
    def wrapper(self):
        return func(self).upper()
    return wrapper

class Player(object):
    def __init__(self):
        self.name = 'Chris'
        self.family = 'Paul'
    
    @p_decorate
    def get_fullname(self):
        return self.name + " " + self.family

cp3 = Player()
print cp3.get_fullname() # CHRIS PAUL

A much better approach would be to make our decorator useful for functions and methods alike. This can be done by putting args and *kwargs as parameters for the wrapper, then it can accept any arbitrary number of arguments and keyword arguments.

def p_decorate(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

class Player(object):
    def __init__(self):
        self.name = 'Chris'
        self.family = 'Paul'
    
    @p_decorate
    def get_fullname(self):
        return self.name + " " + self.family

cp3 = Player()
print cp3.get_fullname() # CHRIS PAUL

Function Object Link to heading

We can also construct a function object using class, with a __call__ member function inside.

class FuncObj(object):
    def __init__(self, name):
        print 'init'
        self.name = name
    
    def __call__(self):
        print 'hello', self.name

fo = FuncObj('chris') # init
fo() # hello chris

From the example above, we infer that function calling can be decomposed to two steps: construct and call. Base on this, a decorator implemented by class is feasible.

Decorator Implemented by Class Link to heading

class make_upper(object):
    def __init__(self, func):
        print 'init'
        self.func = func 

    def __call__(self):
        print 'call'
        print self.func().upper()

# init
@make_upper
def get_name():
    return 'chris'

# call
# CHRIS
get_name()

The code above is much alike with the following without @ syntax:

# call __init__
name = make_upper(get_name)

# call
# CHRIS
name()

Reference