Functions are similar to objects in Python  

  • An object has a class name and an address:

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    p1 = Person("John", 36)
    print(p1)
    <__main__.Person object at 0x7f093be7d850>
    

  • A function has the class function and an address:

    # This is how to "create" a function "object":
    def f1():
        print("Called f1")
    
    print(f1)
    <function f1 at 0x7f093c0ba2a0>
    

DEMO: progs/func-as-obj.py

  Function as a parameter  

  • Functions can have functions as parameters:

    def f1():
        print("Called f1")
    
    def f2(f):     # f is a function parameter
        f()
    
    f2( f1 )
    
    Called f1
    

  • Functions in Python are in fact like an object:

    • Functions can be stored in a variable

    • Functions can be passed as a parameter

    • Functions can also be returned by a function !!!

DEMO: progs/func-as-param.py

  Wrapper functions: returning a function  

  • Example of wrapper function:

    def w(func):
        def wrapper():           # This function is LOCAL to w( )
            print("Started")
            func()
            print("Ended")
    
        return wrapper           # Returns a function !
    
    
    def f1():
        print("Hello")
    
    f2 = w(f1)      # Pass f1 to w() and receive a NEW function f2
    f2()            # Call the new function f2()
    
    Started
    Hello
    Ended
    

DEMO: progs/wrapper.py

  The function name is actually a variable  

  • We can assign a function to a function name:

    def w(func):
        def wrapper():           # This function is LOCAL to w( )
            print("Started")
            func()
            print("Ended")
    
        return wrapper           # Returns a function !
    
    
    def f1():
        print("Hello")
    
    f1()   Hello
    
    # f1 is a VARIABLE and can be assigned....
    
    f1 = w(f1)      # Pass f1 to w() and receive a NEW function
    f1()   Started Hello Ended
    
    f1 = w(f1)      # Pass f1 to w() and receive a NEW function
    f1()   Started Started Hello Ended Ended
    

DEMO: progs/wrapper2.py

  Decorators  

  • Decorator notation:

    def w(func):
        def wrapper():           # This function is LOCAL to w( )
            print("Started")
            func()
            print("Ended")
    
        return wrapper           # Returns a function !
    
    # Instead of writing:
    def f1():
        print("Hello")
    
    f1 = w(f1)      # Pass f1 to w() and receive a NEW function f2
    
    f1()     Started Hello Ended
    
    # We can write: ("decorator notation")
    @w
    def f2():
        print("Hello")
    
    f2()     Started Hello Ended
    

DEMO: progs/decorator.py

  Using * args and ** kwargs to create general decorators  

  • A function with a parameter will break the previous example:

    def w(func):
        def wrapper():           # This function is LOCAL to w( )
            print("Started")
            func()               # *** Function has NO parameter !
            print("Ended")
    
        return wrapper           # Returns a function !
    
    # Instead of writing:
    def f1(s):
        print(s)
    
    f1 = w(f1)      # Pass f1 to w() and receive a NEW function f2
    
    f1("New") 
    TypeError: w.wrapper() takes 0 positional arguments but 1 was given
    # We can write: ("decorator notation")
    @w
    def f2(s):      # Function has 1 parameter
        print(s)
    
    f2("New")   
    

DEMO: progs/decorator2.py

  Using * args and ** kwargs to create general decorators  

  • Solution: capture the positional args with *args and the keyword args with **kwargs:

    def w(func):
        def wrapper(*args, **kwargs):          
            print("Started")
            func(*args, **kwargs) # *** Function has general parameters !
            print("Ended")
    
        return wrapper           # Returns a function !
    
    # Instead of writing:
    def f1(s):
        print(s)
    
    f1 = w(f1)      # Pass f1 to w() and receive a NEW function f2
    
    f1("New")     Started New Ended
    
    # We can write: ("decorator notation")
    @w
    def f2(s):      # Function has 1 parameter
        print(s)
    
    f2("New")     Started New Ended  
    

DEMO: progs/decorator3.py