blog
blog copied to clipboard
Python Function arguments and mutability
1. Function arguments
Arguments to functions are passed by object reference, a concept known in Python as pass-by-assignment. When a function is called, new local variables are created in the function's local namespace by binding the names in the parameter list to the passed arguments.
def birthday(age):
'''Celebrate birthday!'''
age = age + 1
timmy_age = 7
birthday(timmy_age)
print('Timmy is', timmy_age) # Output: Timmy is 7
# 1. timmy_age and age reference the same object.
# 2. Assigning the parameter age with a new value doesn't change timmy_age.
# 3. Since timmy_age has not changed, "Timmy is 7" is displayed.
1.1 Keyword Arguments and Positional Arguments
- Python provides for keyword arguments that allow arguments to map to parameters by name, instead of implicitly by position in the argument list. Good practice is to use keyword arguments for any function containing more than approximately 4 arguments.
- Keyword arguments can be mixed with positional arguments, provided that the keyword arguments come last.
- A function can have a default parameter value for one or more parameters, meaning that a function call can optionally omit an argument, and the default parameter value will be substituted for the corresponding omitted argument.
def student_info(stu_id, name, pronouns, age, school="Hogwarts"):
print(f'{name}, {stu_id}, {pronouns}, Age: {age}, {school}')
student_info(123, name='Bob', pronouns='he/him/his', age=18)
# Bob, 123, he/him/his, Age: 18, Hogwarts
2. Mutability of the argument object
The semantics of passing object references as arguments is important because modifying an argument that is referenced elsewhere in the program may cause side effects outside of the function scope. When a function modifies a parameter, whether or not that modification is seen outside the scope of the function depends on the mutability of the argument object.
- If the object is immutable, such as a string or integer, then the modification is limited to inside the function. Any modification to an immutable object results in the creation of a new object in the function's local scope, thus leaving the original argument object unchanged.
- If the object is mutable, then in-place modification of the object can be seen outside the scope of the function. Any operation like adding elements to a container or sorting a list that is performed within a function will also affect any other variables in the program that reference the same object.
2.1 How to pass a mutable object to a function
The following program illustrates how the modification of a list argument's elements inside a function persists outside of the function call.
def modify(num_list):
num_list[1] = 99
my_list = [10, 20, 30]
modify(my_list)
print(my_list) # my_list still contains 99!
Sometimes a programmer needs to pass a mutable object to a function but wants to make sure that the function does not modify the object at all.
One method to avoid unwanted changes is to pass a copy of the object as the argument instead, like in the statement my_func(num_list[:])
:
def modify(num_list):
num_list[1] = 99 # Modifying only the copy
my_list = [10, 20, 30]
modify(my_list[:]) # Pass a copy of the list
print(my_list) # my_list does not contain 99!
2.2 How to provide a mutable object as a default parameter
A common error is to provide a mutable object, like a list, as a default parameter. Such a definition can be problematic because the default argument object is created only once, at the time the function is defined (when the script is loaded), and not every time the function is called. Modification of the default parameter object will persist across function calls, which is likely not what a programmer intended. The below program demonstrates the problem with mutable default objects and illustrates a solution that creates a new empty list each time the function is called:
""" Default object modification
This program shows a function append_to_list() that has an empty list as default value of my_list.
A programmer might expect that each time the function is called without specifying my_list, a new
empty list will be created and the result of the function will be [value].
However, the default object persists across function calls. """
def append_to_list(value, my_list=[]):
my_list.append(value)
return my_list
numbers = append_to_list(50) # default list appended with 50
print(numbers) # [50]
numbers = append_to_list(100) # default list appended with 100
print(numbers) # [50, 100]
""" Solution: Make a new list
The solution replaces the default list with None, checking for that value, and then creating a new
empty list in the local scope if necessary. """
# Use default parameter value of None
def append_to_list(value, my_list=None):
if my_list == None:
# Create a new list if a list was not provided
my_list = []
my_list.append(value)
return my_list
numbers = append_to_list(50) # default list appended with 50
print(numbers) # [50]
numbers = append_to_list(100) # default list appended with 100
print(numbers) # [100]
Reference: zyBooks > Principles in Information Technology and Computation > 12.8 Function arguments & 12.9 Keyword arguments and default parameter values