- 5.1 Function Definitions
- 5.2 Default Arguments
- 5.3 Variadic Arguments
- 5.4 Keyword Arguments
- 5.5 Variadic Keyword Arguments
- 5.6 Functions Accepting All Inputs
- 5.7 Positional-Only Arguments
- 5.8 Names, Documentation Strings, and Type Hints
- 5.9 Function Application and Parameter Passing
- 5.10 Return Values
- 5.11 Error Handling
- 5.12 Scoping Rules
- 5.13 Recursion
- 5.14 The lambda Expression
- 5.15 Higher-Order Functions
- 5.16 Argument Passing in Callback Functions
- 5.17 Returning Results from Callbacks
- 5.18 Decorators
- 5.19 Map, Filter, and Reduce
- 5.20 Function Introspection, Attributes, and Signatures
- 5.21 Environment Inspection
- 5.22 Dynamic Code Execution and Creation
- 5.23 Asynchronous Functions and await
- 5.24 Final Words: Thoughts on Functions and Composition
5.9 Function Application and Parameter Passing
When a function is called, the function parameters are local names that get bound to the passed input objects. Python passes the supplied objects to the function “as is” without any extra copying. Care is required if mutable objects, such as lists or dictionaries, are passed. If changes are made, those changes are reflected in the original object. Here’s an example:
def square(items): for i, x in enumerate(items): items[i] = x * x # Modify items in-place a = [1, 2, 3, 4, 5] square(a) # Changes a to [1, 4, 9, 16, 25]
Functions that mutate their input values, or change the state of other parts of the program behind the scenes, are said to have “side effects.” As a general rule, side effects are best avoided. They can become a source of subtle programming errors as programs grow in size and complexity—it may not be obvious from reading a function call if a function has side effects or not. Such functions also interact poorly with programs involving threads and concurrency since side effects typically need to be protected by locks.
It’s important to make a distinction between modifying an object and reassigning a variable name. Consider this function:
def sum_squares(items): items = [x*x for x in items] # Reassign "items" name return sum(items) a = [1, 2, 3, 4, 5] result = sum_squares(a) print(a) # [1, 2, 3, 4, 5] (Unchanged)
In this example, it appears as if the sum_squares() function might be overwriting the passed items variable. Yes, the local items label is reassigned to a new value. But the original input value (a) is not changed by that operation. Instead, the local variable name items is bound to a completely different object—the result of the internal list comprehension. There is a difference between assigning a variable name and modifying an object. When you assign a value to a name, you’re not overwriting the object that was already there—you’re just reassigning the name to a different object.
Stylistically, it is common for functions with side effects to return None as a result. As an example, consider the sort() method of a list:
>>> items = [10, 3, 2, 9, 5] >>> items.sort() # Observe: no return value >>> items [2, 3, 5, 9, 10] >>>
The sort() method performs an in-place sort of list items. It returns no result. The lack of a result is a strong indicator of a side effect—in this case, the elements of the list got rearranged.
Sometimes you already have data in a sequence or a mapping that you’d like to pass to a function. To do this, you can use * and ** in function invocations. For example:
def func(x, y, z): ... s = (1, 2, 3) # Pass a sequence as arguments result = func(*s) # Pass a mapping as keyword arguments d = { 'x':1, 'y':2, 'z':3 } result = func(**d)
You may be taking data from multiple sources or even supplying some of the arguments explicitly, and it will all work as long as the function gets all of its required arguments, there is no duplication, and everything in its calling signature aligns properly. You can even use * and ** more than once in the same function call. If you’re missing an argument or specify duplicate values for an argument, you’ll get an error. Python will never let you call a function with arguments that don’t satisfy its signature.