Python Decorator

What is a Python Decorator

Essentially, Python Decorator is a type of Python function.
A Python decorator enables additional features without changing the original function codes.
With a decorator, we may extract codes not irrelevant to the original functions and add flexibility.

Common decorators are:
- internal decorator
- class decorator
- function decorator
- function decorator with args

Function Decorator

For example, to add a logging feature to a function

Not so smart way

def foo():
    print("foo is running")
    print("This is a foo")

Here print out foo is not reusable.

Calling an External Function

We can build an external function and call it within foo

def use_logger(func_name):
    print("{} is running".format(func_name))

def foo():
    use_logger("foo")
    print("This is foo")

But this method, an argument “foo” is still passed.

Decorator

Can build a wrapper method in the function


def use_logger(func):
    def wrapper(*args, **kwargs):
        print("{} is running".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

def foo():
    print("This is foo")

foo = use_logger(foo)
foo()

The output would be

foo is running
This is foo

More commonly, decorator is used like this:

@use_logger
def bar():
    print("This is a bar")

bar()

The results:

bar is running
This is a bar

Function Decorator with Parameters

It’s actually not that different from the function decorator without parameters.

def use_logger(level="debug"):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print("{a} {b} is running".format(a=level, b=func.__name__))
            return func(*args, **kwargs)
        return wrapper
    return decorator

@use_logger("task")
def bar():
    print("this is a bar")
bar()

The output will be

task bar is running
this is a bar

Downside of Using Decorators

  • The disadvantages of decorators is lack of original function’s info e.g. doctring, __name__, parameter list etc.
def use_logger(level="debug"):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print("{a} {b} is running".format(a=level, b=func.__name__))
            return func(*args, **kwargs)
        return wrapper
    return decorator

@use_logger("task")
def bar():
    print("this is a bar")

f = bar

# run f
f()

print(f.__name__)
print(f.__doc__)

the output is below

task bar is running
this is a bar

# f.__name__
wrapper

# f.__doc__
none

The name printed is wrapper but what we actually want is bar. One solution is to rewrite the function as below

from functools import wraps 
def use_logger(level="debug"):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("{a} {b} is running".format(a=level, b=func.__name__))
            return func(*args, **kwargs)
        return wrapper
    return decorator

Let’s run

print(bar.__name__)

The output will be

bar

Class Decorator

  • Class Decorators can use __call__ method, when @ is applied on functions, this method will be invoked
class Foo:

    def __init__(self, func):
        self._func = func

    def __call__(self):
        print("Class decorator running")
        self._func()
        print("Class decorator ending")

@Foo
def bar():
    print("bar")

bar()

The output will be

Class decorator running
bar
Class decorator ending

Internal Decorator

Some internal decorators can be used to restrict a value range, e.g.

class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.__gpa = gpa

    # the method gpa is now can be accessed via propery
    @property
    def gpa(self):
        return self.__gpa

    # the gpa setter is limited by the assertion
    @gpa.setter
    def gpa(self, gpa):
        assert gpa > 0 and gpa <= 4.0, "invalid gpa"
        self.__gpa = gpa

s = Student("HBS", 4.0)
print(s.gpa)

s.gpa = 3.5
print(s.gpa)

The output will look like:

4.0
3.5

   Reprint policy


《Python Decorator》 by Isaac Zhou is licensed under a Creative Commons Attribution 4.0 International License
  TOC