Execute Program

Python for Programmers: Variadic Function Calls

Welcome to the Variadic Function Calls lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We've seen variadic functions, which take an arbitrary number of arguments. We can also call functions with arbitrary numbers of arguments. When we call some_function(*some_list), each value in some_list becomes a separate argument to the function. It's like calling f(some_list[0], some_list[1], ...).

  • >
    def add(x, y):
    return x + y

    numbers = [1, 2]
    add(*numbers)
    Result:
    3Pass Icon
  • That example works because the function takes two arguments, and our numbers list has two elements. The two list elements become two separate arguments to the function. If we try to pass too many arguments or too few, Python raises an exception.

  • >
    def add(x, y):
    return x + y

    numbers = [1, 2, 3]
    add(*numbers)
    Result:
    TypeError: add() takes 2 positional arguments but 3 were givenPass Icon
  • >
    def negate(x):
    return -x

    numbers = [1, 2]
    negate(*numbers)
    Result:
    TypeError: negate() takes 1 positional argument but 2 were givenPass Icon
  • >
    def negate(x):
    return -x

    numbers = [1]
    negate(*numbers)
    Result:
    -1Pass Icon
  • Variadic function calls are often used with variadic functions. The function takes an arbitrary number of arguments, and we pass it an arbitrary number of arguments.

  • >
    def product(*args):
    result = 1
    for arg in args:
    result *= arg
    return result
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    args = [2, 4]
    product(*args)
    Result:
    8Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    args = [2, 3, 4]
    product(*args)
    Result:
    24Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    args = []
    product(*args)
    Result:
    1Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    product(*[5, 6])
    Result:
    30Pass Icon
  • We can also make variadic function calls with dictionaries. Each item in the dictionary becomes a kwarg. The dictionary's keys are the arguments' names, and the dictionary's values are the arguments' values.

  • Note that the function below is a regular, non-variadic function. But we're calling it variadically! We use ** rather than * since we're passing kwargs.

  • >
    def product(a, b):
    return a * b

    args = {
    "a": 2,
    "b": 4,
    }

    # This is identical to `product(a=2, b=4)`.
    product(**args)
    Result:
    8Pass Icon
  • As usual, we have to pass the correct number of arguments. This is true no matter how we call it, even when calling it variadically with **!

  • >
    def product(a, b):
    return a * b

    args = {
    "a": 2,
    "b": 4,
    "c": 6,
    }
    product(**args)
    Result:
    TypeError: product() got an unexpected keyword argument 'c'Pass Icon
  • In an earlier lesson, we saw that functions are first-class values. We'll use that fact in the next example.

  • The function below accepts another function as a regular positional argument, along with some *args and **kwargs. Then it calls the function, passing the *args and **kwargs along.

  • >
    def double_result(f, *args, **kwargs):
    return 2 * f(*args, **kwargs)
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    def product(a, b):
    return a * b

    double_result(product, 2, 3)
    Result:
    12Pass Icon
  • double_result works with any number of arguments, as long as the argument count matches what the passed function expects.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    def negate(x):
    return -x

    double_result(negate, 8)
    Result:
    -16Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    def always_3():
    return 3

    double_result(always_3)
    Result:
    6Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    def add_four_numbers(a, b, c, d):
    return a + b + c + d

    double_result(add_four_numbers, 1, 2, 3, 4)
    Result:
    20Pass Icon
  • The double_result function passes the arguments along, no matter how many there are. Still, if we pass the wrong number of arguments, we get an exception. The exception happens when double_result calls the underlying function like negate or always_3 or add_four_numbers.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    def add_four_numbers(a, b, c, d):
    return a + b + c + d

    double_result(add_four_numbers, 1, 2, 3, 4, 5)
    Result:
    TypeError: add_four_numbers() takes 4 positional arguments but 5 were givenPass Icon
  • So far we've only used lists for the variadic arguments, but we can use any iterable (that is, any data structure that we can loop over). This is a great example of how powerful and universal Python's iterable system is!

  • For example, we've seen that list(some_dict) gives us the dictionary's keys. We can also call a function with *some_dict, which turns each key into a separate positional argument. The function call doesn't care whether *some_iterable is a list, a tuple, a dict, or a special iterable object defined by a third-party library. They all work in the same way.

  • >
    def add(x, y):
    return x + y

    numbers = {
    1: "one",
    2: "two",
    }

    add(*numbers)
    Result:
    3Pass Icon
  • Although that does work, we don't recommend actually passing a dict in this way. If we want to pass the dict's keys as *args, it would be better to explicitly call .keys(). That's 7 more characters, but it makes our intent very clear, whereas add(*some_dict) looks like a mistake at first.

  • >
    def add(x, y):
    return x + y

    numbers = {
    1: "one",
    2: "two",
    }

    add(*numbers.keys())
    Result:
    3Pass Icon
  • We've now seen the main features of Python's function arguments. There are more nuances in Python than in many other languages, but they're there for good reasons.

  • Python's flexibility allows us to:

    • Pass positional arguments, like all other languages: f(1, 2).
    • Name our arguments, even when those arguments were declared as positional: make_cat("Keanu", age=2).
    • Pass arguments out of order when naming them: make_cat(age=4, name="Ms.Fluff").
    • Pass (and accept) variable numbers of arguments: sum(*args).
    • Pass (and accept) variable numbers of kwargs: return_kwargs(**kwargs).
  • A given function definition or function call can use any mixture of these features. It's a wonderful perk of Python, and you might miss it when working in other languages!

  • Here's a code problem:

    Write a function, pipeline, to help us write code in a pipelined style. By "pipelined", we mean calling a number of functions sequentially, like h(g(f(x))).

    pipeline should take an initial value and a variable number of functions. It calls each function in order, passing in the previous function's return value. For example, pipeline(12, add_four, double) should return double(add_four(12)), which is 32.

    A hint: you can use a loop to call one function at a time, updating a local variable each time.

    def pipeline(initial_value, *functions):
    result = initial_value
    for function in functions:
    result = function(result)
    return result
    # Define some testing functions that we'll pipeline together.
    def double(x):
    return 2 * x

    def add_four(x):
    return x + 4

    def stringify(x):
    return str(x)

    def append_0(s):
    return s + "0"

    def integerify(s):
    return int(s)

    assert pipeline(5) == 5
    assert pipeline(4, double) == 8
    assert pipeline(12, add_four, stringify) == "16"
    assert pipeline("10", append_0, integerify) == 100
    assert pipeline(12, add_four, double, stringify, append_0, integerify) == 320
    Goal:
    None
    Yours:
    NonePass Icon