Skip to content
Permalink
7ae7d3de3a
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time

Functions

Functions are the way in which we encapsulate code so that it can be reused multiple times. Sometimes this is in a single program and other times it's to include inside our own custom library.

Any time code needs to be executed or evaluated in the same way, it can also be passed into a function to have the same process done to it. By passing functions to functions we can build up any complex arrangement you can think of, and is how all modern programs are constructed.

Some functions inside python are built in functions that do a specific job or exhibit a specific behaviour, the print function is one for example. There are also ways to perform small inline functions in this manner. This will be covered in the built in functions section. Let's look at some of the reasons we use functions in more depth:

Scope

Scope is the technical term for if python can see a variable or function at any point in your program. As scope is one of those things that makes more sense when you see it, consider this example:

{{ code_from_file("functions/intro-example.py", 6, 23, execute=true) }}

!!! Example In this example, you can see that we have created a variable named "variable_1" and we have created a function using the key word "def", for more information on creating functions you can find it here in the user functions section.

Knowing when a variable or another function is out of scope is very important in programming. We use scope specifically to allow us to avoid naming collisions, but it's also has unwanted side effects if you're not conscious of it. Consider this example:

{{ code_from_file("functions/intro-example.py", 25, 43, execute=true) }}

!!! example In this example we are trying to use the internal variables of all of the functions. The program can only see the variables above it and inside the function that has the try functions. This is because the rules of scope in python search for things in a certain order, but must allow for isolation of functions so that names can be reused and variables don't get accidentally modified.

Python looks for variables and functions in its scope using the following rule...

The LEGB rule

LEGB stands for; local, enclosing, global and built-in in that order. Python when first looking for the location of the variable or function you have called looks in the local scope first. The local scope is ONLY within the function it is called in. Next it looks at the enclosing scope, if the function is in another function then it will look at the enclosing function (the outer function) to see if the variable is there. Next is global, global is best defined as variables that are outside of any other function, so far most of our examples have declared variables in the global scope. Finally it checks the built-ins such as print, more of these keywords can be found in the built-in functions section.

Abstraction and reusability

To understand abstraction, you must first understand how we make code reusable. Consider this code:

{{ code_from_file("functions/intro-example.py", 46, 55, execute=true) }}

This code does the thing many times, but without functions its long to type and would be a pain if we needed to find every addition in a large code base say, if we needed to add 1 to every sum. Consider this function instead:

{{ code_from_file("functions/intro-example.py", 58, 72, execute=true) }}

!!! example See how much cleaner that looks!! We can now modify one function and it will affect all the times it's been called by the rest of the program. We can also write another thousand lines of code and as soon as we need the addition again we can call on it again saving us some time and energy. Because we named it something that makes sense it's very readable and easy to remember when we're using it later on in the development life cycle.

This kind of reusability is paramount and is also how we can keep code modular, more on that in a minute. The point is that we can take that reusable code now and use it all over the code with a simple call. But it also only adds two integers, could we make is even more helpful?? This is what abstraction is for, by making the code more abstract we can make it so that it takes in any number of integers and floats and adds them together. Consider this code:

{{ code_from_file("functions/intro-example.py", 75, 93, execute=true) }}

Now we've made the code take in an arbitrary number of arguments and add them all together, so our function can be used to add any number of integers or floats and still print the result. This type of abstraction can add extra functionality to your code to future proof it further along the development life cycle, but there is also such a thing as premature optimisation that can hold you back from creating a balanced working program sooner. !!! note Don't worry if much of this feels a little advanced for you right now, much of this is broken down in the later sections and explained in greater detail. It's only important that you understand that these principles exist and very roughly how they work at this stage.

Modularity

The term modular programming lends itself to the idea that each function contains all of the code required to do a single job, and then every time you need to do that job again the function is called. If you think about how a character in a computer game is controlled, every time the character turns left it must call a turn left function. The same if it needs to jump or run backwards, directional animations and everything that makes a game run, all could be their own independent functions containing all the code to do each specific job. Consider this pseudo code:

    # outline of a modular program
    def open_file():
        <code that opens files>
        <code that opens files>
        <code that opens files>
    
    def process_file_data():
        <code that reads files>
        <code that reads files>
        <code that reads files>
    
    def close_file():
        <code that closes files>
        <code that closes files>
        <code that closes files>
    
    open_file()
    process_file_data()
    close_file()

In this pseudo code, you can see that we have three functions all doing different jobs. Each one of the functions can contain code for the testing and for completing the steps required to do the job for which its named. In this way, you create modular code that can be used later in the program or exported into other projects, because you can call the function into another file. You can find more information on modules in the modules section here.

{{ todo("remove this outline, for guide only.") }}

Outline

outline user defined - abstraction and reusability - modularity - namespace separation - how a function is called and how it is defined - passing arguments - keyword argument rules - mutable default parameters - pass by value and pass by reference - return statement - side effects - variable length argument lists - tuple packing and unpacking - dictionary packing and unpacking - docstrings - dunders - function annotations

outline built-in functions - generators - yield - lambda - map - Special mentions - any - exec - print