Python — Functions
vvrubelInvoking a function
Even though invoking functions in Python is not about casting a spell or the like, it does sometimes work wonders. Let's start with the concept.
Basically, a function is a structured fragment of code we may want to use in more than one place and more than one time.
For another thing, functions allow us to read both our code and that of someone else way better. Haven't they become your favorite yet?
Here is a simple function call:
multiply(1, 7)
Here multiply is the name of the function, and numbers in parentheses (1, 7) are its arguments. What is an argument? Well, it's just a value, that will be used inside the body of the function. Let's go deeper into it!
§1. Invoking print()
To call, or invoke, a function in your program, simply write its name and add parentheses after it. That's all it takes! Fun fact: if you've ever typed an expression like this print("Hello, world!"), you already know a little about functions. In this tiny example, however, we see the message "Hello, world!" in the parentheses after the name of the print function. What does it mean? This string is just an argument. And more often than not, functions do have arguments. As for the print function, we may as well use it with no argument at all or even with multiple arguments:
print("Hello, world!")
print()
print("Bye,", "then!")
And here is the output:
Hello, world! Bye, then!
So, the first call prints one string, the second call of print without arguments prints, in fact, an empty line, and the last call outputs our two messages as one expression. Are you surprised by these results? Then you may learn how the print function works in more detail from its documentation. The Python documentation contains all sorts of information about the function of your interest, for example, which arguments it expects.
§2. Built-in functions
Functions can make life easier, provided one is aware of their existence. Many algorithms are already written, so there is no need for reinvention, except perhaps for educational purposes.
The Python interpreter has a number of functions and types built into it, so they are always available. Currently, the number of built-in functions amounts to 69 (in the latest version Python 3.8).
Some of them are used to convert the object type, for example, str() returns a string, int() returns an integer, float() returns a floating-point number.
Others deal with numbers: you can round() them and sum() them, find the minimum min() or the maximum max().
Still others give us information about the object: its type() or length len(). Let's consider them in action!
In the following example, len() counts the number of characters in the string (the same goes for any sequence).
number = "111" # finding the length of an object print(len(number)) # 3
Then we declare the variables integer and float_number and write their sum into my_sum. By the way, the sum() function also deals with sequences.
# converting types integer = int(number) float_number = float(number) print(str(float_number)) # "111.0" # adding and rounding numbers my_sum = sum((integer, float_number))
The sum() results in a floating-point number, which becomes clear after printing my_sum.
print(my_sum) # 222.0 print(round(my_sum)) # 222
Furthermore, you can see how to find the minimum and maximum values: the smallest number equals 111 (int) and the largest number 222.0 belongs to floats.
# finding the minimum and the maximum print(min(integer, float_number)) # 111 print(type(max(integer, float_number, my_sum))) # <class 'float'>
There is far more to explore, but let's sum it up.
§3. Recap
The beauty of functions is that we can use them without clear insight into their inner structure and how they manage to perform what we need. But if you want to put a function to good use, make sure to check its documentation or try invoking the special function help() with the name of the function in question wrapped in parentheses. It may become necessary if, for example, the function does not return any value, writes processed data to a file or prints the output on the screen.
Let's make a brief summary:
- functions are meant to be reusable, which means we are free to apply it multiple times with different arguments,
- to call a function, write its name followed by parentheses and place the arguments within,
- normally, a function has documentation, which sometimes might be of huge help.
Declaring a function
Often enough, built-in functions cannot suffice even beginners. In such a case, there is no choice but to create your own function using the keyword def (right, derived from define). Let's have a look at the syntax:
def function_name(parameter1, parameter2, ...): # function's body ... return "return value"
After def, we write the name of our function (so as to invoke it later) and the names of parameters, which our function can accept, enclosed in parentheses. Do not miss the colon at the end of the line. The names of a function and its parameters follow the same convention as variable names, that is, they should be written in lowercase with underscores between words.
An indent of 4 spaces shows the interpreter where the function's body starts and where it ends. All statements in the function's body must be indented. You can make calculations inside your function and use the return keyword to send the result back. Only when the indentation is absent, the definition of the function ends.
Later, the parameters take on values passed in a function call. Those values we pass to a function are known as arguments. The only distinction between parameters and arguments is that we introduce parameters in a function definition and give arguments (some specific values) in a function call. Here is a bit less abstract example of a function:
# Function definition def multiply(x, y): return x * y # Function calls a = multiply(3, 5) # 15 b = multiply(a, 10) # 150
In case you don't want to pass any arguments, the round brackets remain empty:
def welcome():
print("Hello, people!")
You can also declare a sort of empty function with pass statement:
# This function does nothing (yet) def lazy_func(param): pass
When you choose to call lazy_func() with an arbitrary value as its argument, nothing will happen. So pass is just a placeholder, but at least your code will be valid with it.
§1. Parameters vs arguments
It's not quite clear right now, what the parameters are, is it? In fact:
parameters are just aliases for values, which can be passed to a function.
Consider the following example:
def send_postcard(address, message):
print("Sending a postcard to", address)
print("With the message:", message)
send_postcard("Hilton, 97", "Hello, bro!")
# Sending a postcard to Hilton, 97
# With the message: Hello, bro!
send_postcard("Piccadilly, London", "Hi, London!")
# Sending a postcard to Piccadilly, London
# With the message: Hi, London!
As you can see, this function is a reusable piece of code, that can be executed with different arguments, i.e. different values passed into this function. Here, address and message are just the aliases under which the function receives values and then processes them in the body.
This function takes exactly 2 arguments, so you will not be able to execute it with more or less than 2 arguments:
send_postcard("Big Ben, London")
TypeError: send_postcard() missing 1 required positional argument: 'message'
§2. Execution and return
Our previous function only performed some actions, but it didn't have any return value. However, you might want to calculate something in a function and return the result at some point. Check the following example:
def celsius_to_fahrenheit(temps_c): temps_f = temps_c * 9 / 5 + 32 return round(temps_f, 2) # Convert the boiling point of water water_bp = celsius_to_fahrenheit(100) print(water_bp) # 212.0
The keyword return is used to indicate what values the function outputs. Basically, it is the result of the function call. So, in the example above, we've stored the value returned by our function in the variable water_bp. Just to be sure, we printed the result.
One more thing to say is that functions do not necessarily have return values. The well-known print() function does not, in fact, return anything. Examine the code below:
chant = print("We Will Rock You")
print(chant)
And its output:
We Will Rock You None
We declared the variable chant and invoked print(). Obviously, the function was executed. But the variable itself turned out to be the None object, which means the called function had nothing to return. The value of chant is None.
Python interpreter stops performing the function after return. But what if the function body contains more than one return statement? Then the execution will end after the first one. Please, keep that in mind!§3. Conclusion
Thus, we've learned the syntax for declaring functions. Now you also know that:
- Parameters of a function are simply aliases, or placeholders, for values that you will pass to them. Parameters are re-initialized every time you call the function. Inside the function, you have access to these values, which means you can perform calculations on them.
- A function can simply perform an action without returning anything or return a specific result. If your function doesn't return anything, assigning its result to a variable or printing it will give you
None.
Declaring your own functions makes your code more structured and reusable. Whenever you use the same piece of code more than once, try to create a function of it!
Scopes
A scope is a part of the program where a certain variable can be reached by its name. The scope is a very important concept in programming because it defines the visibility of a name within the code block.
§1. Global vs. Local
When you define a variable it becomes either global or local. If a variable is defined at the top-level of the module it is considered global. That means that you can refer to this variable from every code block in your program. Global variables can be useful when you need to share state information or some configuration between different functions. For example, you can store the name of a current user in a global variable and then use it where needed. It makes your code easier to change: in order to set a new user name you will only have to change a single variable.
Local variables are created when you define them in the body of a function. So its name can only be resolved inside the current function's scope. It lets you avoid issues with side-effects that may happen when using global variables.
Consider the example to see the difference between global and local variables:
phrase = "Let it be" def global_printer(): print(phrase) # we can use phrase because it's a global variable global_printer() # Let it be is printed print(phrase) # we can also print it directly phrase = "Hey Jude" global_printer() # Hey Jude is now printed because we changed the value of phrase def printer(): local_phrase = "Yesterday" print(local_phrase) # local_phrase is a local variable printer() # Yesterday is printed as expected print(local_phrase) # NameError is raised
Thus, a global variable can be accessed both from the top-level of the module and the function's body. On the other hand, a local variable is only visible inside the nearest scope and cannot be accessed from the outside.
§2. LEGB rule
A variable resolution in Python follows the LEGB rule. That means that the interpreter looks for a name in the following order:
- Locals. Variables defined within the function body and not declared global.
- Enclosing. Names of the local scope in all enclosing functions from inner to outer.
- Globals. Names defined at the top-level of a module or declared global with a
globalkeyword. - Built-in. Any built-in name in Python.
Let's consider an example to illustrate the LEGB rule:
x = "global" def outer(): x = "outer local" def inner(): x = "inner local" def func(): x = "func local" print(x) func() inner() outer() # "func local"
When the print() function inside the func() is called the interpreter needs to resolve the name x. It'll first look at the innermost variables and will search for the local definition of x in func() function. In the case of the code above, the interpreter will find the local x in func() successfully and print its value, 'func local'. But what if there isn't a definition of x in func()? Then, the interpreter will move outward and turn to inner() function. Check out the following example:
x = "global" def outer(): x = "outer local" def inner(): x = "inner local" def func(): print(x) func() inner() outer() # "inner local"
As you see, the name x was resolved in inner() function, since the value "inner local" was printed.
If we remove the definition of x from the inner() function as well and run the code again, the interpreter will continue the search among the outer() locals in the same fashion. If we keep deleting the lines of code defining x, the interpreter will move on to outer() locals, then globals, and then built-in names. In case there is no matching built-in name, an error will be raised. Look at the example where the global definition of x is reached by the interpreter:
x = "global" def outer(): def inner(): def func(): print(x) func() inner() outer() # "global"
Don't forget about LEGB rule if you plan on using enclosing functions.
§3. Keywords "nonlocal" and "global"
We already mentioned one way to assign a global variable: make a definition at the top-level of a module. But there is also a special keyword global that allows us to declare a variable global inside a function's body.
You can't change the value of a global variable inside the function without using the global keyword:
x = 1 def print_global(): print(x) print_global() # 1 def modify_global(): print(x) x = x + 1 modify_global() # UnboundLocalError
An error is raised because we are trying to assign to a local variable x the expression that contains x and the interpreter can't find this variable in a local scope. To fix this error, we need to declare x global:
x = 1 def global_func(): global x print(x) x = x + 1 global_func() # 1 global_func() # 2 global_func() # 3
When x is global you can increment its value inside the function.
nonlocal keyword lets us assign to variables in the outer (but not global) scope:
def func():
x = 1
def inner():
x = 2
print("inner:", x)
inner()
print("outer:", x)
def nonlocal_func():
x = 1
def inner():
nonlocal x
x = 2
print("inner:", x)
inner()
print("outer:", x)
func() # inner: 2
# outer: 1
nonlocal_func() # inner: 2
# outer: 2
Though global and nonlocal are present in the language, they are not often used in practice, because these keywords make programs less predictable and harder to understand.
§4. Why do we need scopes?
First of all, why does Python need the distinction between global and local scope? Well, from the experience of some other programming languages that do not have local scopes it became clear, that using only global scope is highly inconvenient: when every variable is accessible from every part of the code, a whole bunch of bugs is inevitable. The longer the code, the more difficult it becomes to remember all the variables' names and not accidentally change the value of the variable that you were supposed to keep untouched. Therefore, Python saves you the trouble by allowing you to "isolate" some variables from the rest of the code when you split it into functions.
On the other hand, why do we need global scope then? Well, as was already mentioned above, global scope is one of the easiest ways to retain information between function calls: while local variables disappear the moment the function returns, global variables remain and help functions to transfer the necessary data between each other. Similarly, global variables can enable the communication between more complex processes, such as threads in multithreaded applications.
Arguments
By now, you are on good terms with functions, since you know how to invoke and declare them. Let's deepen your knowledge a bit and discover some new features of functions.
First, a line should be drawn between the terms "argument" and "parameter". Parameters represent what a function accepts, it's those names that appear in the function definition. Meanwhile, arguments are the values we pass to a function when calling it. We'll cover both arguments and parameters further.
§1. Positional arguments
There are different ways to assign arguments to a function. First of all, you can do it simply by position. In this case, values will associate with parameters in the order in which you passed them into your function from left to right. Such arguments are called positional, or non-keyword.
def subtract(x, y): return x - y subtract(11, 4) # 7 subtract(4, 11) # -7
When we swapped the numbers in the second function call, we got a different result. Thus, you can see that the order determines how arguments are assigned.
§2. Named arguments
Another way to assign arguments is by name. Sometimes you might want to control the order of passed values. That's where named, or keyword, arguments come into play.
def greet(name, surname):
print("Hello,", name, surname)
# Non-keyword arguments
greet("Willy", "Wonka") # Hello, Willy Wonka
# Keyword arguments
greet(surname="Wonka", name="Willy") # Hello, Willy Wonka
The order doesn't matter here since parameters are matched by name. However, keyword arguments are always written after non-keyword arguments when you call a function:
greet("Frodo", surname="Baggins") # Hello, Frodo Baggins
greet(name="Frodo", "Baggins") # SyntaxError: positional argument follows keyword argument
Make sure to mention each parameter once. To understand why this is important, let's think about what happens every time we call a function. In fact, arguments are initialized so that all operations with the values in this function start from scratch. You cannot initialize an argument twice, so if a value has already been passed and associated with some parameter, attempts to assign another value to this name will fail.
def greet(name, surname):
print("Hello,", name, surname)
greet("Clementine", name="Rose")
# TypeError: greet() got multiple values for argument 'name'
As shown in the example, multiple values for the same name cause an error.
§3. Names are important
We have covered the main errors that you can face. Of course, there can be more parameters in a function:
def responsibility(developer, tester, project_manager, designer): print(developer, "writes code") print(tester, "tests the system") print(project_manager, "manages the product") print(designer, "develops design")
Note that when we use keyword arguments, names are important, not positions. Thus, the following example will work correctly:
responsibility(project_manager="Sara", developer="Abdul", tester="Yana", designer="Mark") # Abdul writes code # Yana tests the system # Sara manages the product # Mark develops design
However, if we call the function with the same order of names, but without named arguments, then the output will be wrong, with mixed responsibilities:
responsibility("Sara", "Abdul", "Yana", "Mark")
# Sara writes code
# Abdul tests the system
# Yana manages the product
# Mark develops design
This way, Python knows the names of the arguments that our function takes. We can ask to remind us of them using the built-in help() function.
help(responsibility) # Help on function responsibility in module __main__: # responsibility(developer, tester, project_manager, designer)
§4. PEP time
Look at the declared function and function calls shown in this topic one more time: greet(name="Willy", surname="Wonka"). Have you noticed missing spaces around the equality sign? Their absence is not accidental. By PEP 8 convention, you should not put spaces around = when indicating a keyword argument.
§5. Conclusions
Now, when we've discussed some advanced features of functions, let's sum it up:
- There's a distinction between parameters and arguments.
- You can pass arguments to a function by position and by name.
- The order of declared parameters is important, as well as the order of arguments passed into a function.
- The
help()function can tell you the function arguments by name.
Any and all
By now, you certainly know that Python has a lot of different built-in functions that help developers work more efficiently. Built-in functions are always available, so you don't need to declare or import them. Just call such a function whenever you need it. You have already seen one of those functions, it is print(). Today we will learn two more built-in functions any() and all() and find out how and when to use them.
Be careful though, these functions work only with iterable objects, e.g. strings and lists. A list is iterable, so we will use it to illustrate the theoretical part and to show how any() and all() work.
§1. Function any()
The result of the any() function call is the boolean value: it returns True if an element or a group of elements in an iterable object are evaluated True. Otherwise, it returns False. Let’s take a look at the example. Imagine that you and your friends, Jam and Andy, wrote a test and got your results in the form of a list with True and False values. The test is passed if at least one answer is correct. Now you need to check if you and your friends passed that test.
your_results = [True, False, False] print(any(your_results)) # True
As you know, the value True corresponds to 1, while False can be represented by 0, therefore, you can replace the boolean values with the numerical ones in the list above and get the same result.
your_results = [1, 0, 0] print(any(your_results)) # True
In fact, all numbers other than 0 will be regarded as True, even the negative ones. That part stems from how the bool() function works.
Now back to our test example, you passed with one correct answer. What about your friends, Jam and Andy? Andy has a different list of results to check.
andy_results = [False, False, False] print(any(andy_results)) # False
Unfortunately, your friend Andy failed. What about Jam? Well, this friend of yours didn't write the test at all, so he got an empty list of results.
jam_results = [] print(any(jam_results)) # False
The list doesn't contain any elements, and since no True value is to be found the any() function returns False.
So what does the any() function do? First, it takes a list as an argument, then evaluates all the elements of this list to find at least one True, if so, it returns True, otherwise, the result is False.
§2. Function all()
The all() function works pretty much like any(). The difference is that all() function checks if all the elements of an iterable object are True and returns True if they are. Otherwise, you get False. Do you remember the story from the previous section where we checked the results of the test? Let's proceed. Imagine yet another test, this time the final one. To succeed, you should answer all the questions correctly. How did it go this time for you and the two friends of yours?
your_results = [True, False, False] print(all(your_results)) # False
As you can see, not all the answers in your case are correct, so you didn't pass the test. What about Andy's results?
andy_results = [True, True, True] print(all(andy_results)) # True
Luckily, Andy passed. Jam seems to have a vacation. His list of results is empty again.
jam_results = [] print(all(jam_results)) # True
The list doesn't contain any elements, but the all() function will return True because it searches for any False values. No False values result in True. Be careful with this scenario.
§3. Non-boolean values
Pay attention to the fact that any() and all() can take a list containing non-boolean values. Let's recall how they are evaluated.
Empty sequences, e.g. strings and lists, as well as zero, are equivalent to False, the same applies to the constant None. Non-empty sequences are equivalent to True.
Be cautious, the result of calling the all() function on an empty sequence differs from converting an empty sequence to a boolean value. The result of all(list()) is True, the result of bool(list()) is False.
Here is a list with false values. any() and all() will have the same behavior in this example:
rocket_science_scores = [0, -0, 0.0, +0] any(rocket_science_scores) # False all(rocket_science_scores) # False
Now, let's look at the scores of some simpler subject:
math_scores = [0, 1, 2, 3] any(math_scores) # True all(math_scores) # False
As shown, all() doesn't return True for a list where false values are present. Consider the last case:
biology_scores = [1, 2, 3, 4] any(biology_scores) # True all(biology_scores) # True
The list biology_scores has no false values, that's why both functions result in True.
Also, you can turn the elements of your list into the boolean values via comparison. Suppose, we have a list of scores and want to check whether some are equal to 3 or greater. It can be done like this:
scores = [1, 2, 3, 4] boolean_scores = [score >= 3 for score in scores] # [False, False, True, True] print(any(boolean_scores)) # True print(all(boolean_scores)) # False
However, lists may contain different elements, e.g. strings or nested lists, in such cases, the behavior of any() and all() would depend largely on the contents. Keep that in mind.
§4. Conditions
Coders often use any() and all() functions in conditions. It helps to check the elements of iterable objects quickly and to avoid complex constructions.
Let's choose a candy box for Valentine's Day. Each box contains several types of sweets. But you are interested in the even amount of candies of each type because, obviously, you will share them with your valentine.
box = [10, 20, 33]
if any([candy % 2 for candy in box]):
print("It is not a proper gift.")
else:
print("Perfect!")
Short and sweet, isn't it? Life is like a box of chocolates! As long as the values you deal with can be converted to True and False, it's safe to use both functions in conditions.
§5. To sum up
We learned what any() and all() functions can do and how they work. As you can see, these functions are an efficient tool that helps check conditions and may improve the readability of your code.