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


  1. Default args
    1. Default args example
    2. Do not use mutable default args
    3. One workaround for mutable default args
  2. Variable length args (*args)
  3. Keyword args (**kwargs)
  4. Functions as parameters
  5. Lambda functions
  6. Functions inside functions
  7. Closures + Non-local variables
  8. Non-local variables fail on setting (use nonlocal)
  9. Functions that return functions
  10. Function decorators

  1. Default args  
    1. Default args example
      def f(x, y=10): return (x,y)
      print(f(5))   # (5, 10)
      print(f(5,6)) # (5, 6)

    2. Do not use mutable default args
      def f(x, L=[ ]):
          L.append(x)
          return L
      
      print(f(1))
      print(f(2)) # why is this [1, 2]?

    3. One workaround for mutable default args
      def f(x, L=None):
          if (L == None):
              L = [ ]
          L.append(x)
          return L
      
      print(f(1))
      print(f(2)) # [2] (that's better)

  2. Variable length args (*args)  
    def longestWord(*args):
        if (len(args) == 0): return None
        result = args[0]
        for word in args:
            if (len(word) > len(result)):
                result = word
        return result
    
    print(longestWord("this", "is", "really", "nice")) # really
    
    mywords = ["this", "is", "really", "nice"]
    
    print(longestWord(mywords))  # ['this', 'is', 'really', 'nice']
    print(longestWord(*mywords)) # really

  3. Keyword args (**kwargs)  
    def f(x=1, y=2): return (x,y)
    print(f()) # (1, 2)
    print(f(3)) # (3, 2)
    print(f(y=3)) # (1, 3) [here is where we use a keyword arg]
    
    def f(x, **kwargs): return (x, kwargs)
    print(f(1)) # (1, { })
    print(f(2, y=3, z=4)) # (2, {'z': 4, 'y': 3})

  4. Functions as parameters  
    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)

  5. Lambda functions  
    print(derivative(lambda x:3*x**5 + 2, 2)) # about 240, 15*x**4 at x==2
    
    myF = lambda x: 10*x + 42
    print(myF(5)) # 92
    print(derivative(myF, 5)) # about 10

  6. Functions inside functions  
    def f(L):
        def squared(x): return x**2
        return [squared(x) for x in L]
    print(f(range(5)))
    try:
        print(squared(5))
    except:
        print("squared is not defined outside f")

  7. Closures + Non-local variables  
    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)))

  8. Non-local variables fail on setting (use nonlocal)  
    def brokenF(L):
        lastX = 0
        def squared(x):
            result = x**2
            lastX = x
            return result
        squaredList = [squared(x) for x in L]
        return lastX
    print(brokenF(range(5)))
    
    def fixedF(L):
        lastX = 0
        def squared(x):
            nonlocal lastX
            result = x**2
            lastX = x
            return result
        squaredList = [squared(x) for x in L]
        return lastX
    print(fixedF(range(5)))

  9. Functions that return functions  
    def derivativeFn(f):
        def g(x):
            h = 10**-5
            return (f(x+h) - f(x))/h
        return g
    
    def f(x): return 5*x**3 + 10
    fprime1 = derivativeFn(f)
    fprime2 = derivativeFn(fprime1)
    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

  10. Function decorators  
    @derivativeFn
    def h(x): return 5*x**3 + 10
    print(h(3)) # 135, matches fprime1 from above.