Lecture 2.2
Guoliang Ma
The Chow Institute, 2026
“Functions are first-class objects.”
Functions are also objects.
Functions as arguments.
Functions as return values.
We’ll rely on several specific use cases to introduce these topics.
AoL 2 (H)
First-class objects are flexible. Being first class means there is no restrictions on the use of the object. We can pass this object as an argument to a function and can return it as a return value. We can also create dictionaries to store it, etc.
When we use a function as an argument and return values of another "higher-level" functions, we are using higher-order functions.
Functions as return values
def intercept_1():
a = 1
def slope_2(x):
return 2 * x + a
return slope_2
linear_trans = intercept_1()
linear_trans(3)
Functions as arguments
def call_count(func, x=[0]):
print(f"calling {x[0] + 1} times")
x[0] += 1
func()
call_count(print)
call_count(print)
call_count(print)
AoL 3 (M)
Modify code example 1, so that we can select the intercept.
Modify code example 1, so that we can also select the slope.
Modify code example 2, so that we do not need default parameters.
In the call_count example, we can pass a function as an argument to the function. But this function cannot have its own parameters. How can we pass arguments to the function being counted?
def call_count(func, arg_to_called, x=[0]):
print(f"calling {x[0]} times")
x[0] += 1
func(arg_to_called)
call_count(print, "hello")
call_count(print, "python")
call_count(print, "world")
AoL 3 (M)
When we are not sure about how many parameters to pass to the function, the conventional parameter names are args and kwargs. The special syntax is *args and **kwargs; the names themselves are not keywords.
The * operator. A star is known as the (un)packing operator.
How do they differ?
a = 1, 2, 3
a, b, c = 1, 2, 3
a, b = 1, 2, 3
a, *b, c = 1, 2, 3, 4, 5
AoL 3 (M)
Some cases deliberately contain errors. Consider and run each assignment separately.
Summarize the pattern by considering
*a, b = 1, 2, 3, 4, 5
a, *b = 1, 2, 3, 4, 5
*a, *b = 1, 2, 3, 4, 5
*a, b, c = 1, 2, 3, 4, 5
*a, b = 1
Note that a is a list but *a unpacks the list into several elements. Passing indefinite number of arguments to a function involves two steps:
collecting positional arguments in a tuple (when defining *args)
unpacking an iterable with * when making a call
To check the unpacking behavior, we can use the sep parameter.
def call_count(func, *args, x=[0]):
print(f"calling {x[0]} times")
x[0] += 1
func(*args, sep=", ")
call_count(print, "hello", "python", "world")
The ** operator.
Another type of arguments is called keyword arguments, which must be passed to a function with the form param=arg. These are named arguments. Unlike * that unpacks a list, we use ** to unpack a dictionary. There are fewer use cases than the unpacking of a list.
dict1 = {"a": 1,
"b": 2,
"c": 3}
dict2 = {"d": 4,
"e": 5,
"f": 6}
combined_dict = {**dict1, **dict2}
Ordinary positional arguments precede keyword arguments in a call:
print("hello", "Python", sep=", ")
In the positional parameter list of a function definition, required parameters come before parameters with defaults.
Keyword-only parameters follow * or *args; **kwargs, when present, comes last.
def function(required, optional=0, *args, keyword_only, **kwargs):
...
AoL 3 (M)
Note that **kwargs is actually unpacking a dict. This is to say, kwargs is a dict. We learned that a dict has keys and values. Read the document about named arguments: https://docs.python.org/3/library/stdtypes.html#dict
Write a function to take the sum of several (the numbers are unknown) named arguments. For example,
def sum_of_kwargs(???):
pass
sum_of_kwargs(Alice=5, Bob=3, Charlie=4)