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