Function definition

So far we have been writting very small pieces of code, but in most reallistic projects we will need to structure our code, we will have to divide our software in pieces, and functions are one of the main ways of accomplishing that.

You can think on a programming function like in a kind of mathematical function, that takes some parameters and returns a result (although as we will see both the parameters and the result are optional).

flowchart LR
  radius(["radius"])-- calc_area -->area(["area"])

When we are programming what we do is to manipulate data. Think in a program as a series of manipulations on the data that is stored in memory. We take some pieces of data, for example the radius of a circle, and we do a action, like calculating the area. So we need ways of defining that data, and for that we use types (and classes), and we create functions (and methods) to manipulate that data.

Imagine data as things/objects or properties of those objects, and functions as actions that act on the data. For example, we have some data, the radius of a circle, we apply an action to it, calc_area, and we obtain a new piece of data, the area. Both radius and area are pieces of data, whereas cacl_area is an action, a verb.

Resources

Definition and calling

In Python functions are defined by using the def keyword.

If you run the previous piece of code you won’t see any result. Once you define a function, the function will be ready to be used, but in order to use it you need to call it. We call a function by using parentheses (()).

Tip

The lines that belong to the function are the lines indented after the function definition line. Those are the lines in the function block block. Fix the following code:

Tip

It is important to understand how the flow, the order in which the lines are executed, is changed by the function. Once you call the function the flow goes into the function until the function ends. At that point the flow is sent back to the line that called the function.

Try to fill out the numbers in the prints of the following code to track the flow of execution. In which order will be the lines executed?

Tip

Scopes and arguments

The code inside a function has its own variables, and those are not shared between functions and between the function and the rest of the code. To use a value inside a value, usually, we pass the variable to the function. To understand what do we mean when we say that we pass some data to a function we need to understand the concept of the scope.

When we are programming we store and access data in memory. As we have seen, we refer to the data stored in memory by using variables. We could think that those variables, once they are created, are available in every part of our program. If you used that approach when you tried to build a program with more than a few lines of code, it would become very difficult to track which part of the program had changed a variable. So maintining those large programs would be very difficult.

The scope defines where in the code a variable is available, and functions define their own scope. In a computer program not all data is available to every part of the code. For instance, the variables that are defined inside a function are not available outside of the function.

We say that the variables defined in the function are in the function scope, that means that they are not available ouside of the function.

We get a “‘name’ is not defined error” because the variable name was created inside the say_hello function, so in the say_hello scope, and is not available outside. A variable can only be used when is in the current scope. It is said that the function has a local scope in which its variables are available.

Schema with the scope of a function call:

flowchart LR
    subgraph def["def calc_area(width, height):"]
        subgraph func_scope["Function scope"]
            subgraph return
               ret["return area"]
            end
            subgraph in["function variables"]
               var["area = width * height"]
            end
            subgraph arguments
               width
               height
            end
        end
    end
    subgraph result["result = calc_area(2, 3)"]
        subgraph caller["Caller scope"]
            subgraph result_[" "]
               result__["result ="]
            end
            subgraph funccall["Function call"]
               call1["calc_area(2, 3)"]
            end
        end
    end
    return ---> result__
    call1 ---> arguments

Global scope

Be careful because Python also has a global scope, and if you create the variable outside the function it will be available inside. We can use the variables defined in the global scope inside any function.

In this case the variable name has been created in the global scope, it is a global variable available everywhere. In general, avoid creating global variables, specially if they are not inmutable constants. As a general rule the use of the global scope is discouraged, try not to use it. If you think you need a global variable, think twice, in most cases is better not to use them. This is an advanced topic, but here’s a tip for the future you, to keep states it might be much better to use objects, instances of a class, that global variables. For now, this is too advanced. Just remember, try very hard not to use global varibles, although you can allow some exceptions with some inmutable ones used for global configurations. It is common to use this kind of global variables, inmutables and related to configuration, and, by convention, in Python, people name them using all caps variable names.

Python discourages some uses of the use the global scope, and the behaviour of these variables is atypical.

Passing data to a function, function arguments

So, when the function requires some data to carry an action, we should pass that data explictly to the function. For instance, if we want a function to print a personalized greeting, it could need the name of the person.

Passing data to a function is easy, but a lot is going on under the hood:

  1. We have created a text string (“Jane”). That means that Python has created and stored a object of type str in memory.

flowchart TB
    subgraph main [ ]
    person:::invisible
    end
    subgraph say [ ]
    name:::invisible
    end
    Memory:::memory
    subgraph Memory
    Jane["'Jane'"]
    end
    person --> Jane
    name --> Jane
    classDef variable fill:#f96
    classDef invisible opacity:0%
    classDef memory fill:#ccc
    linkStyle 0,1 stroke-width:0px

  1. We have assigned the variable person to that string, so now person refers to that str object stored in memory.

flowchart TB
    subgraph main [global scope]
    person
    end
    subgraph say [ ]
    name:::invisible
    end
    Memory:::memory
    subgraph Memory
    Jane["'Jane'"]:::variable
    end
    person --> Jane
    name --> Jane
    classDef invisible opacity:0%
    classDef memory fill:#ccc
    linkStyle 1 stroke-width:0px

  1. When we call the function we pass the reference of the object, the variable person, to the function.
  2. The function receives the reference to the str object and assigns to it a new reference, in this case called name. It is very important to understand that although the object is the same, the “Jane” string, we have created a new reference to it, the new variable name.

flowchart TB
    subgraph main [global scope]
    person
    end
    subgraph say [say_hello scope]
    name
    end
    Memory:::memory
    subgraph Memory
    Jane["'Jane'"]
    end
    person --> Jane
    name --> Jane
    classDef invisible opacity:0%
    classDef memory fill:#ccc

return

Scopes have also to be taken into account when getting a result out of the function.

If we want to get data out of the function we use the return statement. Let’s see how we can return data to the caller.

We use the return statement to return a value generated in the function to the caller. Again, like in the arguments passed to the function, the caller will receive a new reference to the value stored in memory that can assign to a new variable. return is used to move a result in memory between scopes. For instance, in the previous example there is a variable, inside the function, named area, but in the first call we store the reference in a variable named result. area and result are two variables, two references to the same value stored in memory, but they are variables that belong to different scopes. In this case area belongs to the scope of the calc_rect_area function and result to the global scope.

Fix the following code

Note

Does the function return something?

Tip

Write a function that returns “odd” or “even” depending on the number that we pass.

Tip

Write a program with two functions, one that transforms from Fahrenheit to Celsius, and another one from Celsius to Fahrenheit.

Note

Define a function that reverses a list, like the reversed funtion.

Note

Remember that indexing has the posibility of reversing using -1 as the step (some_sequence[::-1]).

Note

Function calls are independent

Note also that each time that the function returns, its scope, the variables created inside the function and available to it, is erased, so each call will be independent. Once a return is executed in a function, the function returns and its scope is destroyed with all its variables.

What would be the result of executing the following code and why?

Tip

Everytime we call a function a new scope with new data is created and the function starts its execution in its first line and ends its execution once it encounters the first return. That’s why no matter how many time we call this function it will always return “Hi” and “Bye” will be never reached.

Methods

So far we haven’t talked about clases and their instances, the objects, because they are a slightly more advanced topic related to Object-oriented programming (OOP).

Python is a object oriented language, in fact, in Python almost everything is an object. And although we are not going to discuss this topic yet, it is important to understand the concept and use of methods, the object “functions”. In fact we have already used methods when dealing with strings.

upper is a method, a function associated with an object. In this case the object is the string “hello” and upper is a function that acts on that object. By convention functions associated to objects are called methods. So upper is a method, a function associated to the objects of type str. You can think on a method like on a function that has as its first argument the given object. If upper were a standard function, the previous code would be:

s = "hello"
s = upper(s)
print(s)

Something similar to that would work if upper were a normal function but, instead, upper is a method associated to an object, so the syntax is different, we use a dot after the object, then the name of the method/function, and then the language makes sure that the object is passed as the first argument to this method.

If you want to understand Python you need to learn about objects, classes, methods and properties, because Python is a deeply object-oriented language, but this is a topic that we can leave for later.

Anonymous functions, lambdas

In Python, and many other programming languages, we can create anonymous functions, a function with no name. This is a more abstract idea, but they are used quite commonly in Python, so it would be nice, at least, that you could recognized them.

In Python anonymous functions are created with the lambda statement. lambdas are usually very small functions, just one line, and they are used as parameters for other functions. For instance, they are very common in as the key argument to the sorted function.

You can learn more about lambdas in the official documentation or in Real Python.