Decorator

Decorator

1.1 Modifying the function

  • decorator is more of a design pattern
  • for exmaple
# f1 prints a simple sentence
def f1():
    print("This is a function")
  • Now, f1 doesn’t fit my need, I want to print out time as well
import time
def f1_old():
    print(time.time())
    print("This is a function")
# it prints out a Unix timestamp and sentence
f1_old()
1549295628.544087
This is a function

1.2 How about modifying/decorating multiple functions

  • The above method is fine for decorating one function, however if there are hundreds of functions, it will not be feasible
  • When requirements change, we should not change the actual function or class definitions
  • Close to modification, Open for extension
  • To solve the issue of batch modifying functions, we can define a new function
# define three functions
def f1():
    print("This is a function")
def f2():
    print("This is another function")
def f3():
    pass
import time
def print_current_time(func):
    print(time.time())
    func()
    print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
print_current_time(f1)
print_current_time(f2)
print_current_time(f3)
1549295812.4483118
This is a function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1549295812.4483118
This is another function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1549295812.4483118
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  • The above solution, print_current_time is not really associated with each f1, f2, f3
  • It will be essentially the same as: python print(time.time()) f1() print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~") print(time.time()) f2() print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~") print(time.time()) f3() print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~")

1.3 Introducing Decorators

import time
# Define a decorator
def decorator(func):
    def wrapper():
        # wrapper has the real logic and processes
        print(time.time())
        func()
    return wrapper

def f1():
    print("This is a function")
f = decorator(f1)
f()
1549296488.0140028
This is a function
  • The shortcomings of above is we can’t use it directly, we have to assign it to a new varialbe, and invoke that function again
  • we can use @decorator to avoid change function definition or function calling, which is the benefit of using decorators
@decorator
def f2():
    print("This is a function")
# with the above definition, we can call f2 directly
f2()
1549296721.0764937
This is a function

1.4 Why decorator?

  • originally f2 only prints “This is a function”
  • by adding @decorator, f2 can print its original function as well as time
  • We can accept the complexity in definition, but we shouldn’t accept the complexity in calling/invoking
  • decorator is a type of AOP (aspect oriented programming). In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns.

1.5 Adding Parameters

  • The above example doesn’t take any parameters
  • If we want to accept parameters how to change the decorator?
# define decorator
import time
def decorator(func):
    def wrapper(name):
        print(time.time())
        func(name)
    return wrapper

# define function
@decorator
def func3(name):
    print("Hello, World! This is "+name)

# call function
func3("Isaac")
1549297192.66419
Hello, World! This is Isaac

It seems that we have solved the issue, however what if we have another function that accepts two parameters?

@decorator
def func4(name1, name2)
    print("Hello, {}! Greetings from {}".format(name1, name2))
@decorator
def func4(name1, name2):
    print("Hello, {}! Greetings from {}".format(name1, name2))
func4("Isaac", "Allen")
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-47-2eddc50b8ed6> in <module>
      2 def func4(name1, name2):
      3     print("Hello, {}! Greetings from {}".format(name1, name2))
----> 4 func4("Isaac", "Allen")


TypeError: wrapper() takes 1 positional argument but 2 were given

We should use *args

# define decorator
import time
def decorator(func):
    def wrapper(*args):
        print(time.time())
        func(*args)
    return wrapper

# define function
@decorator
def func3(name):
    print("Hello, World! This is {}".format(name))

# call function
func3("Isaac")

@decorator
def func4(name1, name2):
    print("Hello, {}! Greetings from {}".format(name1, name2))
func4("Isaac", "Allen")
1549297529.3837247
Hello, World! This is Isaac
1549297529.3847258
Hello, Isaac! Greetings from Allen

1.6 Adding keyword arguments

  • What if we want to add keyword arguments?
def func5(name1, name2, **kw):
    print("name1: ", name1)
    print("name2: ", name2)
    print(kw)
func5("Isaac", "Allen", a = 1, b = 2, c ="123")
name1:  Isaac
name2:  Allen
{'a': 1, 'b': 2, 'c': '123'}
@decorator
def func5(name1, name2, **kw):
    print("name1: ", name1)
    print("name2: ", name2)
    print(kw)
func5("Isaac", "Allen", a = 1, b = 2, c ="123")
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-52-93593afce8f9> in <module>
      4     print("name2: ", name2)
      5     print(kw)
----> 6 func5("Isaac", "Allen", a = 1, b = 2, c ="123")


TypeError: wrapper() got an unexpected keyword argument 'a'
  • The current decorator cannot accept keywoard arguments
  • The change is easy, just add **kw to the end of *args
import time
def decorator(func):
    def wrapper(*args, **kwargs):
        print(time.time())
        func(*args, **kwargs)
    return wrapper
@decorator
def func_6(name1, name2, **kw):
    print(name1, name2, kw) #note it's kw not **kw
func_6("Isaac", "Allen", a = 1, b = 2, c ="123")
1549298826.3816261
Isaac Allen {'a': 1, 'b': 2, 'c': '123'}

1.7 Summary Decorator Benefits

  • If we want to change behavior of an encapsulation, e.g. a function, we don’t need to change its definition, we just need to use decorators
  • The decorators can be used on other functions repeatedly
  • A function has multiple decorators
  • Below is using decorator in flask
@api.route("/get", methods=["GET"])
def test_javascript_http():
    p = request.args.get("name")
    return p, 200

@api.route("/psw", methods=["GET"])
@auth.login_required
def get_psw():
    p = request.args.get("psw")
    r = generate_password_hash(p)
    return "success", 200