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`

❮ ❯