CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Functions As Objects


 Learning Goal: use functions as objects. In particular:
  1. Functions are objects
  2. Functions can be parameters
  3. Functions can return functions
  4. Function decorators
  5. Nonlocal Scope
    1. Scope of functions inside functions
    2. Closures + Non-local variables
    3. Non-local variables fail on setting (use nonlocal)
  6. Check 10.6

  1. Functions are objects
    Functions are like any other value or object we use in Python. You can print them and set variables to them, using the function's name as a reference.
    def f(): print("foo") g = f g() print(f)

  2. Functions can be parameters
    Since functions are objects, we can pass them in to other functions as parameters. This can help us organize and re-use our code.

    This derivative function takes another function f as an input!
    # returns the derivative of f at x def derivative(f, x): h = 10**-8 return (f(x+h) - f(x))/h def f(x): return 4*x + 3 print(derivative(f, 2)) # about 4 def g(x): return 4*x**2 + 3 print(derivative(g, 2)) # about 16 (8*x at x==2)

  3. Functions can return functions
    We should expect by now that, since functions are just values, we can return functions from other functions too. The following example shows how this can work.

    Note below that we're defining a function inside a function, and then returning the inner function itself! We do this so that the inner function can use the parameters provided to the main function in computation.

    Now we have a new function that calculates the derivative (or second derivative, etc!) of a function at any point, and it's really clean!

    # Yo dawg, I heard you like functions, so I put a function in your function # so you can return functions from your function. def derivativeFn(f): def g(x): #We define a function inside a function h = 10**-8 return (f(x+h) - f(x))/h #This looks familiar... return g #Return the function! Note the lack of () def f(x): return 5*x**3 + 10 fprime1 = derivativeFn(f) #fprime1 is a function fprime2 = derivativeFn(fprime1) #and so is fprime2 print(f(3)) # 145, 5*x**3 + 10 evaluated at x == 3 print(fprime1(3)) # about 135, 15*x**2 evaluated at x == 3 print(fprime2(3)) # about 90, 30*x evaluated at x == 3

    The big picture is that we can use functions to modify and create functions, just like we would any other data type!

    This is a really powerful idea that opens up tons of new possibilities!

  4. Function decorators
    Decorators are functions which take in a function, and output a new function. The @decoratorFn syntax allows us to replace the function below it with the output of the decorator, when the function below it is given as an input to the decorator.
    def stringify(f): def g(*args): return str(f(*args)) return g # Here we replace these functions with the output "g" of the decorator @stringify def double(x): return x * 2 @stringify def mult(x, y): return x * y print(type(double(10))) # <class 'str'> print(type(mult(2, 3))) # <class 'str'>

    Here's another example, this time one that uses a try-catch statement to cover up function crashes. This can be very useful...
    def noCrashes(f): def g(*args): try: return f(*args) except: print("crashed!") return None return g @noCrashes def shouldCrash(): raise Exception shouldCrash() # prints 'crashed!'

  5. The nonlocal scope
    Now that we can define functions inside functions, we have to deal with variable scope between inner and outer functions. This can get tricky...

    1. Scope of functions inside functions
      First, note that functions defined within functions are not accessible outside of their enclosing function.
      def f(L): def squared(x): return x**2 return [squared(x) for x in L] print(f(range(5))) squared(5) # crashes!

    2. Closures + Non-local variables
      The variables inside of function f can be accessed and modified by function squared. These variables act similarly to globals for function squared, but we call them nonlocals.
      def f(L): myMap = dict() def squared(x): result = x**2 myMap[x] = result return result squaredList = [squared(x) for x in L] return myMap print(f(range(5)))

    3. Non-local variables fail on setting (use nonlocal)
      Just like global variables, nonlocals need to be referenced with a special keyword before they can be set.
      def brokenF(L): lastX = 0 def squared(x): result = x**2 lastX = x # if we don't specify nonlocal, we'll recreate this variable! return result squaredList = [squared(x) for x in L] return lastX print(brokenF(range(5))) # Doesn't work def fixedF(L): lastX = 0 def squared(x): nonlocal lastX # this links the variable to the correct scope result = x**2 lastX = x return result squaredList = [squared(x) for x in L] return lastX print(fixedF(range(5)))

  6. Check 10.6