CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Variadic Functions (Advanced Parameters)


 Learning Goal: write functions that take in a variable number of parameters. In particular:
  1. Default args
    1. Mutable default args create aliases
    2. How to fix aliasing
  2. Variadic functions (*args)
  3. Keyword args (**kwargs)
  4. Packing and unpacking arguments
  5. Check 10.5

  1. Default args
    We've used default args implicitly a few times in the course so far.

    print("hello") # vs print("hello", end = " ") run() # vs run(300, 600)

    When an argument isn't necessary for a function, or doesn't need a specific value, we can use default args to allow us to not specify that argument at all!

    This syntax provides the specified variable with a value in case the function is called without that argument.
    def f(x, y=10): return (x, y) print(f(5)) # (5, 10) print(f(5, 6)) # (5, 6)

    If a function has multiple default parameters, we can specify which one we're setting when we call the function. This is called a keyword argument.
    def f(x=10, y=15): return (x, y) print(f(x=20)) # (20, 15) print(f(y=10)) # (10, 10) print(f(x=30, y=40)) # (30, 40)

    1. Mutable default args create aliases
      Objects attached to default arguments in a function definition (like the [ ] attached to lst below) are created only one time, when the function is first defined. This means that each time the function is called, lst refers to the same list.

      This behavior is useful for caching and memoization, but can cause problems if you want to call a function multiple times on different inputs.
      def f(x, lst=[ ]): lst.append(x) return lst print(f(1)) print(f(2)) # why is this [1, 2]?

    2. How to fix aliasing
      How can we stop this aliasing from occurring? One approach is to set the default arg to None, then check for None in the function itself, and set the value there. This defines the new mutable argument every time the function is called without it.
      def f(x, lst=None): if (lst == None): lst = [ ] lst.append(x) return lst print(f(1)) print(f(2)) # [2] (that's better)

      Another way to fix this problem is to make a wrapper function that passes the initial state you want (like [ ]) to the function.
      def fHelper(x, lst): lst.append(x) return lst def f(x): return fHelper(x, [ ]) print(f(1)) print(f(2)) # [2] (that's nice too)

  2. Variadic functions (*args)
    You can allow your functions to take a variable number of arguments using *args! We call these functions variadic functions.

    print and canvas.create_polygon are great examples of variadic functions we've used heavily in the course.

    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

    We can also have some required arguments in our variadic function, to specify a minimum number.
    a, b, c, d, e = "yay", 10, [3,4], 3.5, ("dog, cat") def f(thing1, thing2, *args): print("First thing: ", thing1) print("Second thing:", thing2) print("Other things:", args) # Note that args is just a tuple, nothing fancy print() f(a,b,c,d,e) # Pass a bunch of stuff in! f(b,d) # Pass in our two required things print(f(c)) # Crashes

  3. Keyword args (**kwargs)
    Functions can also take in a variable number of named, or keyword, parameters.

    canvas.create_text is a great example of this kind of function. It can take in a font, anchor, text, fill, offset, state, tags, and more; all named parameters!

    # We can specify that we might get keyword args, even if we don't know what they'll be def f(x, **kwargs): return (x, kwargs) # This is a little like how we used *args, but we store them as a dictionary instead print(f(1)) # (1, { }) print(f(2, y=3, z=4)) # (2, {'z': 4, 'y': 3}) # You can even use *args and **kwargs at the same time. Neat! def g(*args, **kwargs): return (args, kwargs) print(g(1,2,3,x=7,y=8,z=9)) # The ordering does matter though, so you can't scramble them like this: #print(g(1,x=7,2,3,y=8,z=9)) #Crashes! #print(g(x=7,y=8,z=9,1,2,3)) #Also crashes!

    Here's an example of how we can use **kwargs:
    def describePerson(**kwargs): print(kwargs.get('name', 'Somebody'), "has", end=" ") for key in kwargs.keys(): if key == 'name': continue print("%s %s," % (kwargs[key], key), end = " ") print() describePerson(name='Jon', hair='red', eyes='green') describePerson(name='Alex', hair='brown', eyes='blue', eyeshadow='purple') describePerson(hair='orange')

  4. Packing and unpacking arguments
    One big question remains: what is the * in *args actually doing?

    The first argument to print2 below is a group of "packed arguments". The * in *args tells python to "unpack" those arguments and place the resulting tuple into "args".

    When we run print(*args) we "pack" that tuple into a set of arguments, which print can use as normal.

    Packing and unpacking arguments will show up more in future CS courses. For now, it's okay to just use *args without fulling understanding those concepts.

    def print2(*args): print(args) # right now it's a tuple print(*args) # we pack it back up for the function print2("This", "is", "basically", "print")

    Note that any iterable can be packed up for a function.
    print(*[1,2,3,4]) # lists work print(*(1,2,3,4)) # tuples work print(*{1,2,3,4}) # sets work

    Unpacking and packing up keyword arguments works the same way, but we use ** instead.
    def print3(*args, **kwargs): print(*args, **kwargs) print3("This","is","literally","print",end="!\n")

    Here's an example of how packing and unpacking can be used.
    def describePerson(**kwargs): print(kwargs.get('name', 'Somebody'), "has", end=" ") for key in kwargs.keys(): if key == 'name': continue print("%s %s," % (kwargs[key], key), end = " ") print() describePerson(**{'name': 'Alex', 'hair': 'brown', 'eyes': 'blue', 'eyeshadow': 'purple'})

  5. Check 10.5