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

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

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

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

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

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

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