From GEOG 485 or similar previous experience, you should be familiar with defining simple functions that take a set of input parameters and potentially return some value. When calling such a function from somewhere in your Python code, you have to provide values (or expressions that evaluate to some value) for each of these parameters, and these values are then accessible under the names of the respective parameters in the code that makes up the body of the function.
However, from working with different tool functions provided by arcpy and different functions from the Python standard library, you also already know that functions can also have optional parameters, and you can use the names of such parameters to explicitly provide a value for them when calling the function. In this section, we will show you how to write functions with such keyword arguments and functions that take an arbitrary number of parameters, and we will discuss some more details about passing different kinds of values as parameters to a function.
The parameters we have been using so far, for which we only specify a name in the function definition, are called positional parameters or positional arguments because the value that will be assigned to them when the function is called depends on their position in the parameter list: The first positional parameter will be assigned the first value given within the parentheses (…) when the function is called, and so on. Here is a simple function with two positional parameters, one for providing the last name of a person and one for providing a form of address. The function returns a string to greet the person with.
def greet(lastName, formOfAddress): return 'Hello {0} {1}!'.format(formOfAddress, lastName) print(greet('Smith', 'Mrs.'))
Output: Hello Mrs. Smith!
Note how the first value used in the function call (“Smith”) in line 6 is assigned to the first positional parameter (lastName) and the second value (“Mrs.”) to the second positional parameter (formOfAddress). Nothing new here so far.
The parameter list of a function definition can also contain one or more so-called keyword arguments. A keyword argument appears in the parameter list as
<argument name> = <default value>
A keyword argument can be provided in the function by again using the notation
def greet(lastName, formOfAddress, language = 'English'): greetings = { 'English': 'Hello', 'Spanish': 'Hola' } return '{0} {1} {2}!'.format(greetings[language], formOfAddress, lastName) print(greet('Smith', 'Mrs.')) print(greet('Rodriguez', 'Sr.', language = 'Spanish'))
Output: Hello Mrs. Smith! Hola Sr. Rodriguez!
Compare the two different ways in which the function is called in lines 8 and 10. In line 8, we do not provide a value for the ‘language’ parameter so the default value ‘English’ is used when looking up the proper greeting in the dictionary stored in variable greetings. In the second version in line 10, the value ‘Spanish’ is provided for the keyword argument ‘language,’ so this is used instead of the default value and the person is greeted with “Hola” instead of "Hello." Keyword arguments can be used like positional arguments meaning the second call could also have been
print(greet('Rodriguez', 'Sr.', 'Spanish'))
without the “language =” before the value.
Things get more interesting when there are several keyword arguments, so let’s add another one for the time of day:
def greet(lastName, formOfAddress, language = 'English', timeOfDay = 'morning'): greetings = { 'English': { 'morning': 'Good morning', 'afternoon': 'Good afternoon' }, 'Spanish': { 'morning': 'Buenos dias', 'afternoon': 'Buenas tardes' } } return '{0}, {1} {2}!'.format(greetings[language][timeOfDay], formOfAddress, lastName) print(greet('Smith', 'Mrs.')) print(greet('Rodriguez', 'Sr.', language = 'Spanish', timeOfDay = 'afternoon'))
Output: Good morning, Mrs. Smith! Buenas tardes, Sr. Rodriguez!
Since we now have four different forms of greetings depending on two parameters (language and time of day), we now store these in a dictionary in variable greetings that for each key (= language) contains another dictionary for the different times of day. For simplicity reasons, we left it at two times of day, namely “morning” and “afternoon.” In line 7, we then first use the variable language as the key to get the inner dictionary based on the given language and then directly follow up with using variable timeOfDay as the key for the inner dictionary.
The two ways we are calling the function in this example are the two extreme cases of (a) providing none of the keyword arguments, in which case default values will be used for both of them (line 10), and (b) providing values for both of them (line 12). However, we could now also just provide a value for the time of day if we want to greet an English person in the afternoon:
print(greet('Rogers', 'Mrs.', timeOfDay = 'afternoon'))
Output: Good afternoon, Mrs. Rogers!
This is an example in which we have to use the prefix “timeOfDay =” because if we leave it out, it will be treated like a positional parameter and used for the parameter ‘language’ instead which will result in an error when looking up the value in the dictionary of languages. For similar reasons, keyword arguments must always come after the positional arguments in the definition of a function and in the call. However, when calling the function, the order of the keyword arguments doesn’t matter, so we can switch the order of ‘language’ and ‘timeOfDay’ in this example:
print(greet('Rodriguez', 'Sr.', timeOfDay = 'afternoon', language = 'Spanish'))
Of course, it is also possible to have function definitions that only use optional keyword arguments in Python.
Let us continue with the “greet” example, but let’s modify it to be a bit simpler again with a single parameter for picking the language, and instead of using last name and form of address we just go with first names. However, we now want to be able to not only greet a single person but arbitrarily many persons, like this:
greet('English', 'Jim', 'Michelle')
Output: Hello Jim! Hello Michelle!
greet('Spanish', 'Jim', 'Michelle', 'Sam')
Output: Hola Jim! Hola Michelle! Hola Sam!
To achieve this, the parameter list of the function needs to end with a special parameter that has a * symbol in front of its name. If you look at the code below, you will see that this parameter is treated like a list in the body of the function:
def greet(language, *names): greetings = { 'English': 'Hello', 'Spanish': 'Hola' } for n in names: print('{0} {1}!'.format(greetings[language], n))
What happens is that all values given to that function from the one corresponding to the parameter with the * on will be placed in a list and assigned to that parameter. This way you can provide as many parameters as you want with the call and the function code can iterate through them in a loop. Please note that for this example we changed things so that the function directly prints out the greetings rather than returning a string.
We also changed language to a positional parameter because if you want to use keyword arguments in combination with an arbitrary number of parameters, you need to write the function in a different way. You then need to provide another special parameter starting with two stars ** and that parameter will be assigned a dictionary with all the keyword arguments provided when the function is called. Here is how this would look if we make language a keyword parameter again:
def greet(*names, **kwargs): greetings = { 'English': 'Hello', 'Spanish': 'Hola' } language = kwargs['language'] if 'language' in kwargs else 'English' for n in names: print('{0} {1}!'.format(greetings[language], n))
If we call this function as
greet('Jim', 'Michelle')
the output will be:
Hello Jim! Hello Michelle!
And if we use
greet('Jim', 'Michelle', 'Sam', language = 'Spanish')
we get:
Hola Jim! Hola Michelle! Hola Sam!
Yes, this is getting quite complicated, and it’s possible that you will never have to write functions with both * and ** parameters, still here is a little explanation: All non-keyword parameters are again collected in a list and assigned to variable names. All keyword parameters are placed in a dictionary using the name appearing before the equal sign as the key, and the dictionary is assigned to variable kwargs. To really make the ‘language’ keyword argument optional, we have added line 5 in which we check if something is stored under the key ‘language’ in the dictionary (this is an example of using the ternary "... if ... else ..." operator). If yes, we use the stored value and assign it to variable language, else we instead use ‘English’ as the default value. In line 9, language is then used to get the correct greeting from the dictionary in variable greetings while looping through the name list in variable names.
When making the transition from a beginner to an intermediate or advanced Python programmer, it also gets important to understand the intricacies of variables used within functions and of passing parameters to functions in detail. First of all, we can distinguish between global and local variables within a Python script. Global variables are defined outside of any function. They can be accessed from anywhere in the script and they exist and keep their values as long as the script is loaded which typically means as long as the Python interpreter into which they are loaded is running.
In contrast, local variables are defined inside a function and can only be accessed in the body of that function. Furthermore, when the body of the function has been executed, its local variables will be discarded and cannot be used anymore to access their current values. A local variable is either a parameter of that function, in which case it is assigned a value immediately when the function is called, or it is introduced in the function body by making an assignment to the name for the first time.
Here are a few examples to illustrate the concepts of global and local variables and how to use them in Python.
def doSomething(x): # parameter x is a local variable of the function count = 1000 * x # local variable count is introduced return count y = 10 # global variable y is introduced print(doSomething(y)) print(count) # this will result in an error print(x) # this will also result in an error
This example introduces one global variable, y, and two local variables, x and count, both part of the function doSomething(…). x is a parameter of the function, while count is introduced in the body of the function in line 3. When this function is called in line 11, the local variable x is created and assigned the value that is currently stored in global variable y, so the integer number 10. Then the body of the function is executed. In line 3, an assignment is made to variable count. Since this variable hasn’t been introduced in the function body before, a new local variable will now be created and assigned the value 10000. After executing the return statement in line 5, both x and count will be discarded. Hence, the two print statements at the end of the code would lead to errors because they try to access variables that do not exist anymore.
Now let’s change the example to the following:
def doSomething(): count = 1000 * y # global variable y is accessed here return count y = 10 print(doSomething())
This example shows that global variable y can also be directly accessed from within the function doSomething(): When Python encounters a variable name that is neither the name of a parameter of that function nor has been introduced via an assignment previously in the body of that function, it will look for that variable among the global variables. However, the first version using a parameter instead is usually preferable because then the code in the function doesn’t depend on how you name and use variables outside of it. That makes it much easier to, for instance, re-use the same function in different projects.
So maybe you are wondering whether it is also possible to change the value of a global variable from within a function, not just read its value? One attempt to achieve this could be the following:
def doSomething(): count = 1000 y = 5 return count * y y = 10 print(doSomething()) print(y) # output will still be 10 here
However, if you run the code, you will see that last line still produces the output 10, so the global variable y hasn't been changed by the assignment in line 5. That is because the rule is that if a name is encountered on the left side of an assignment in a function, it will be considered a local variable. Since this is the first time an assignment to y is made in the body of the function, a new local variable with that name is created at that point that will overshadow the global variable with the same name until the end of the function has been reached. Instead, you explicitly have to tell Python that a variable name should be interpreted as the name of a global variable by using the keyword ‘global’, like this:
def doSomething(): count = 1000 global y # tells Python to treat y as the name of global variable y = 5 # as a result, global variable y is assigned a new value here return count * y y = 10 print(doSomething()) print(y) # output will now be 5 here
In line 5, we are telling Python that y in this function should refer to the global variable y. As a result, the assignment in line 7 changes the value of the global variable called y and the output of the last line will be 5. While it's good to know how these things work in Python, we again want to emphasize that accessing global variables from within functions should be avoided as much as possible. Passing values via parameters and returning values is usually preferable because it keeps different parts of the code as independent of each other as possible.
So after talking about global vs. local variables, what is the issue with mutable vs. immutable mentioned in the heading? There is an important difference in passing values to a function depending on whether the value is from a mutable or immutable data type. All values of primitive data types like numbers and boolean values in Python are immutable, meaning you cannot change any part of them. On the other hand, we have mutable data types like lists and dictionaries for which it is possible to change their parts: You can, for instance, change one of the elements in a list or what is stored under a particular key in a given dictionary without creating a completely new object.
What about strings and tuples? You may think these are mutable objects, but they are actually immutable. While you can access a single character from a string or element from a tuple, you will get an error message if you try to change it by using it on the left side of the equal sign in an assignment. Moreover, when you use a string method like replace(…) to replace all occurrences of a character by another one, the method cannot change the string object in memory for which it was called but has to construct a new string object and return that to the caller.
Why is that important to know in the context of writing functions? Because mutable and immutable data types are treated differently when provided as a parameter to functions as shown in the following two examples:
def changeIt(x): x = 5 # this does not change the value assigned to y y = 3 changeIt(y) print(y) # will print out 3
As we already discussed above, the parameter x is treated as a local variable in the function body. We can think of it as being assigned a copy of the value that variable y contains when the function is called. As a result, the value of the global variable y doesn’t change and the output produced by the last line is 3. But it only works like this for immutable objects, like numbers in this case! Let’s do the same thing for a list:
def changeIt(x): x[0] = 5 # this will change the list y refers to y = [3,5,7] changeIt(y) print(y) # output will be [5, 5, 7]
The output [5,5,7] produced by the print statement in the last line shows that the assignment in line 3 changed the list object that is stored in global variable y. How is that possible? Well, for values of mutable data types like lists, assigning the value to function parameter x cannot be conceived as creating a copy of that value and, as a result, having the value appear twice in memory. Instead, x is set up to refer to the same list object in memory as y. Therefore, any change made with the help of either variable x or y will change the same list object in memory. When variable x is discarded when the function body has been executed, variable y will still refer to that modified list object. Maybe you have already heard the terms “call-by-value” and “call-by-reference” in the context of assigning values to function parameters in other programming languages. What happens for immutable data types in Python works like “call-by-value,” while what happens to mutable data types works like “call-by-reference.” If you feel like learning more about the details of these concepts, check out this article on Parameter Passing [1].
While the reasons behind these different mechanisms are very technical and related to efficiency, this means it is actually possible to write functions that take parameters of mutable type as input and modify their content. This is common practice (in particular for class objects which are also mutable) and not generally considered bad style because it is based on function parameters and the code in the function body does not have to know anything about what happens outside of the function. Nevertheless, often returning a new object as the return value of the function rather than changing a mutable parameter is preferable. This brings us to the last part of this section.
It happens quite often that you want to hand back different things as the result of a function, for instance four coordinates describing the bounding box of a polygon. But a function can only have one return value. It is common practice in such situations to simply return a tuple with the different components you want to return, so in this case a tuple with the four coordinates. Python has a useful mechanism to help with this by allowing us to assign the elements of a tuple (or other sequences like lists) to several variables in a single assignment. Given a tuple t = (12,3,2,2), instead of writing
top = t[0] left = t[1] bottom = t[2] right = t[3]
you can write
top, left, bottom, right = t
and it will have the exact same effect. The following example illustrates how this can be used with a function that returns a tuple of multiple return values. For simplicity, the function computeBoundingBox() in this example only returns a fixed tuple rather than computing the actual tuple values from a polygon given as input parameter.
def computeBoundingBox(): return (12,3,41,32) top, left, bottom, right = computeBoundingBox() # assigns the four elements of the returned tuple to individual variables print(top) # output: 12
This section has been quite theoretical, but you will often encounter the constructs presented here when reading other people’s Python code and also in the rest of this course.