🤖Functions

FUNCTIONS

Click the links below for alternative Turkish tutorial videos:

Video 1

Video 2

What is a Function?

In Python, a function is a group of related statements that performs a specific task.

  • A process for executing a task

  • It can accept input and return an output

  • Useful for executing similar procedures over and over

Why Use Functions?

  • Stay DRY - Don't Repeat Yourself!

  • Clean up and prevent code duplication

Functions help break our program into smaller and modular chunks. As our program grows larger and larger, functions make it more organized and manageable.

Furthermore, it avoids repetition and makes the code reusable.

Syntax of Function

1 def function_name(parameters):
     """docstring"""
     statement(s)

Above shown is a function definition that consists of the following components.

  1. Keyword def that marks the start of the function header.

  2. A function name to uniquely identify the function.

  3. Parameters (arguments) through which we pass values to a function. They are optional

  4. A colon (:) to mark the end of the function header.

  5. Optional documentation string (docstring) to describe what the function does

  6. One or more valid python statements that make up the function body. Statements must have the same indentation level (usually 4 spaces).

  7. An optional return statement to return a value from the function.

Example of a function

def greet(name):
	    """ This function greets to
         the person passed in as
         a parameter"""
	
			print("Hello, " + name + ". Good morning!")

How to call a function in python?

Once we have defined a function, we can call it from another function, program or even the Python prompt. To call a function we simply type the function name with appropriate parameters.

def greet(name):
    """This function greets to
     the person passed in as
      a parameter"""
	  print("Hello, " + name + ". Good morning!")

greet('Paul')

Docstrings

The first string after the function header is called the docstring and is short for documentation string. It is briefly used to explain what a function does.

Although optional, documentation is a good programming practice. Unless you can remember what you had for dinner last week, always document your code.

In the above example, we have a docstring immediately below the function header. We generally use triple quotes so that docstring can extend up to multiple lines. This string is available to us as the _ _doc _ _ attribute of the function.

>>> print(greet.  doc )

 This function greets to
 the person passed in as
 a parameter

To learn more about docstrings in Python, visit Python Docstrings .

The return statement

The return statement is used to exit a function and go back to the place from where it was called.

return [expression_list]

This statement can contain an expression that gets evaluated and the value is returned. If there is no expression in the statement or the return statement itself is not present inside a function, then the function will return the None object.

 >>> print(greet("May"))
 Hello, May. Good morning!
 None

Here, None is the returned value since greet() directly prints the name and no return statement is used.

def absolute_value(num):
        """This function returns the absolute
         value of the entered number"""

         if num >= 0:
              return num
         else:
              return -num


print(absolute_value(2))

print(absolute_value(-4))

Functions that readily come with Python are called built-in functions. If we use functions written by others in the form of library, it can be termed as library functions.

All the other functions that we write on our own fall under user-defined functions. So, our user-defined function could be a library function to someone else.

Advantages of user-defined functions

  1. User-defined functions help to decompose a large program into small segments which makes program easy to understand, maintain and debug.

  2. If repeated code occurs in a program. Function can be used to include those codes and execute when needed by calling that function.

  3. Programmars working on large project can divide the workload by making different functions.

def add_numbers(x,y):
    sum = x + y
    return sum

num1 = 5
num2 = 6

print("The sum is", add_numbers(num1, num2))

Scope and Lifetime of variables

Scope of a variable is the portion of a program where the variable is recognized. Parameters and variables defined inside a function are not visible from outside the function. Hence, they have a local scope.

The lifetime of a variable is the period throughout which the variable exits in the memory. The lifetime of variables inside a function is as long as the function executes.

They are destroyed once we return from the function. Hence, a function does not remember the value of a variable from its previous calls.

def my_func():
    x = 10
    print("Value inside function:",x)

x = 20
my_func()
print("Value outside function:",x)

Here, we can see that the value of x is 20 initially. Even though the function my_func() changed the value of x to 10, it did not affect the value outside the function.

This is because the variable x inside the function is different (local to the function) from the one outside. Although they have the same names, they are two different variables with different scopes.

On the other hand, variables outside of the function are visible from inside. They have a global scope.

We can read these values from inside the function but cannot change (write) them. In order to modify the value of variables outside the function, they must be declared as global variables using the keyword global .

Python Function Arguments

def greet(name, msg):
    """This function greets to
       the person with the provided message"""
    print("Hello", name + ', ' + msg)

greet("Monica", "Good morning!")

Here, the function greet() has two parameters.

Since we have called this function with two arguments, it runs smoothly and we do not get any error.

If we call it with a different number of arguments, the interpreter will show an error message. Below is a call to this function with one and no arguments along with their respective error messages.

>>> greet("Monica")	# only one argument
TypeError: greet() missing 1 required positional argument: 'msg'

>>> greet()	# no arguments.
TypeError: greet() missing 2 required positional arguments: 'name' and 'msg'

Up until now, functions had a fixed number of arguments. In Python, there are other ways to define a function that can take variable number of arguments.

Python Default Arguments

We can provide a default value to an argument by using the assignment operator (=). Here is an example.

def greet(name, msg="Good morning!"):
				"""This function greets to
            the person with the
             provided message.
           If the message is not provided,
            it defaults to "Good
              morning!""""

	        print("Hello", name + ', ' + msg)


greet("Kate")
greet("Bruce", "How do you do?")

In this function, the parameter name does not have a default value and is required (mandatory) during a call.

On the other hand, the parameter msg has a default value of "Good morning!" . So, it is optional during a call. If a value is provided, it will overwrite the default value.

Any number of arguments in a function can have a default value. But once we have a default argument, all the arguments to its right must also have default values.

This means to say, non-default arguments cannot follow default arguments. For example, if we had defined the function header above as:

def greet(msg = "Good morning!", name):

We would get an error as:

SyntaxError: non-default argument follows default argument

Python Keyword Arguments

When we call a function with some values, these values get assigned to the arguments according to their position.

For example, in the above function greet() , when we called it as greet("Bruce", "How do you do?") , the value "Bruce" gets assigned to the argument name and similarly "How do you do?" to msg .

Python allows functions to be called using keyword arguments. When we call functions in this way, the order (position) of the arguments can be changed. Following calls to the above function are all valid and produce the same result.

1 # 2 keyword arguments
2 greet(name = "Bruce",msg = "How do you do?")
3
4 # 2 keyword arguments (out of order)
5 greet(msg = "How do you do?",name = "Bruce")
6
7 #1 positional, 1 keyword argument
8 greet("Bruce", msg = "How do you do?")

As we can see, we can mix positional arguments with keyword arguments during a function call. But we must keep in mind that keyword arguments must follow positional arguments.

Having a positional argument after keyword arguments will result in errors. For example, the function call as follows:

greet(name="Bruce","How do you do?")

Will result in an error:

SyntaxError: non-keyword arg after keyword arg

Python Arbitrary Arguments

Sometimes, we do not know in advance the number of arguments that will be passed into a function. Python allows us to handle this kind of situation through function calls with an arbitrary number of arguments.

In the function definition, we use an asterisk (*) before the parameter name to denote this kind of argument. Here is an example.

def adder(*num):
    sum = 0
    for n in num:
         sum = sum + n

    print("Sum:",sum)

adder(3,5)
adder(4,5,6,7)
adder(1,2,3,5,6)

Here, we have called the function with multiple arguments. These arguments get wrapped up into a tuple before being passed into the function. Inside the function, we use a for loop to retrieve all the arguments back.

Arbitrary Arguments are often shortened to *args in Python documentations.

Python Arbitrary Keyword Arguments

Python passes variable length non keyword argument to function using args but we cannot use this to pass keyword argument. For this problem Python has got a solution called *kwargs , it allows us to pass the variable length of keyword arguments to the function.

In the function, we use the double asterisk before the parameter name to denote this type of argument. The arguments are passed as a dictionary and these arguments make a dictionary inside function with name same as the parameter excluding double asterisk .

def intro(**data):
    print("\nData type of argument:",type(data))

    for key, value in data.items():
        print("{} is {}".format(key,value))

intro(Firstname="Sita", Lastname="Sharma", Age=22, Phone=1234567890)
intro(Firstname="John", Lastname="Wood", Email="johnwood@nomail.com")

In the above program, we have a function intro() with **data as a parameter. We passed two dictionaries with variable argument length to the intro() function. We have for loop inside intro() function which works on the data of passed dictionary and prints the value of the dictionary.

Things to Remember:

  • args and kwargs are special keyword which allows function to take variable length argument.

  • *args passes variable number of non-keyworded arguments list and on which operation of the list can be performed.

  • **kwargs passes variable number of keyword arguments dictionary to function on which operation of a dictionary can be performed.

  • args and *kwargs make the function flexible.

Python Recursive Function

What is recursion?

Recursion is the process of defining something in terms of itself.

A physical world example would be to place two parallel mirrors facing each other. Any object in between them would be reflected recursively.

In Python, we know that a function can call other functions. It is even possible for the function to call itself. These types of construct are termed as recursive functions.

The following image shows the working of a recursive function called recurse .

Following is an example of a recursive function to find the factorial of an integer.

Factorial of a number is the product of all the integers from 1 to that number. For example, the factorial of 6 (denoted as 6!) is 12345*6 = 720.


1 def factorial(x):
    """This is a recursive function
    to find the factorial of an integer"""

    if x == 1:
        return 1
    else:
        return (x * factorial(x-1))


num = 3
print("The factorial of", num, "is", factorial(num))

In the above example, factorial() is a recursive function as it calls itself.

When we call this function with a positive integer, it will recursively call itself by decreasing the number.

Each function multiplies the number with the factorial of the number below it until it is equal to one. This recursive call can be explained in the following steps.

factorial(3)	# 1st call with 3
3 * factorial(2)	# 2nd call with 2
3 * 2 * factorial(1) # 3rd call with 1
3 * 2 * 1	# return from 3rd call as number=1
3 * 2	# return from 2nd call
6	# return from 1st call

Let's look at an image that shows a step-by-step process of what is going on:

Our recursion ends when the number reduces to 1. This is called the base condition.

Every recursive function must have a base condition that stops the recursion or else the function calls itself infinitely.

The Python interpreter limits the depths of recursion to help avoid infinite recursions, resulting in stack overflows.

By default, the maximum depth of recursion is 1000. If the limit is crossed, it results in RecursionError . Let's look at one such condition.

def recursor():
    recursor()
recursor()

Advantages of Recursion

  1. Recursive functions make the code look clean and elegant.

  2. A complex task can be broken down into simpler sub-problems using recursion.

  3. Sequence generation is easier with recursion than using some nested iteration.

Disadvantages of Recursion

  1. Sometimes the logic behind recursion is hard to follow through

  2. Recursive calls are expensive (inefficient) as they take up a lot of memory and time.

  3. Recursive functions are hard to debug.

Python Anonymous/Lambda Function

In Python, an anonymous function is a function that is defined without a name.

While normal functions are defined using the def keyword in Python, anonymous functions are defined using the lambda keyword.

Hence, anonymous functions are also called lambda functions.

Syntax of Lambda Function in python:

lambda arguments: expression

Lambda functions can have any number of arguments but only one expression. The expression is evaluated and returned. Lambda functions can be used wherever function objects are required.

# Program to show the use of lambda functions
double = lambda x: x * 2

print(double(5))

In the above program, lambda x: x 2 is the lambda function. Here x is the argument and x 2 is the expression that gets evaluated and returned.

This function has no name. It returns a function object which is assigned to the identifier double . We can now call it as a normal function. The statement

double = lambda x: x * 2

is nearly the same as:

def double(x):
   return x * 2

Use of Lambda Function in Python

We use lambda functions when we require a nameless function for a short period of time.

In Python, we generally use it as an argument to a higher-order function (a function that takes in other functions as arguments). Lambda functions are used along with built-in functions like filter() , map() etc.

Example use with filter()

The filter() function in Python takes in a function and a list as arguments.

The function is called with all the items in the list and a new list is returned which contains items for which the function evaluates to True .

Here is an example use of filter() function to filter out only even numbers from a list.

# Program to filter out only the even items from a list
my_list = [1, 5, 4, 6, 8, 11, 3, 12]

new_list = list(filter(lambda x: (x%2 == 0) , my_list))

print(new_list)

Example use with map()

The map() function in Python takes in a function and a list.

The function is called with all the items in the list and a new list is returned which contains items returned by that function for each item.

Here is an example use of map() function to double all the items in a list.

# Program to double each item in a list using map()

my_list = [1, 5, 4, 6, 8, 11, 3, 12]

new_list = list(map(lambda x: x * 2 , my_list))

print(new_list)
names = [
  {'first':'Rusty', 'last': 'Steele'},
  {'first':'Colt', 'last': 'Steele', },
  {'first':'Blue', 'last': 'Steele', }]

first_names = list(map(lambda x: x['first'], names))

print(first_names)

Combining filter and map

names = ['Lassie', 'Colt', 'Rusty']

instructor = list(map(lambda name: f"Your instructor is {name}",
                filter(lambda value: len(value) < 5, names)))
print(instructor)

Python Global, Local and Nonlocal variables

Global Variables

In Python, a variable declared outside of the function or in global scope is known as a global variable. This means that a global variable can be accessed inside or outside of the function.

Let's see an example of how a global variable is created in Python.

x = "global"

def foo():
    print("x inside:", x)


foo()
print("x outside:", x)

In the above code, we created x as a global variable and defined a foo() to print the global variable x . Finally, we call the foo() which will print the value of x .

What if you want to change the value of x inside a function?

x = "global"

def foo():
   	x = x * 2
    print(x)

foo()

The output shows an error because Python treats x as a local variable and x is also not defined inside foo() .

To make this work, we use the global keyword.

Local Variables

A variable declared inside the function's body or in the local scope is known as a local variable.

def foo():
    y = "local"

foo()
print(y)

The output shows an error because we are trying to access a local variable y in a global scope whereas the local variable only works inside foo() or local scope.

Let's see an example on how a local variable is created in Python.

Normally, we declare a variable inside the function to create a local variable.

def foo():
    y = "local"
    print(y)

foo()

Here, we will show how to use global variables and local variables in the same code.

x = "global "

def foo():
    global x
    y = "local"
    x = x * 2
    print(x)
    print(y)

foo()

In the above code, we declare x as a global and y as a local variable in the foo() . Then, we use multiplication operator to modify the global variable x and we print both x and y .

After calling the foo() , the value of x becomes global global because we used the x 2 to print two times global . After that, we print the value of local variable y i.e local .

Global variable and Local variable with same name

x = 5

def foo():
    x = 10
    print("local x:", x)


foo()
print("global x:", x)

In the above code, we used the same name x for both global variable and local variable. We get a different result when we print the same variable because the variable is declared in both scopes, i.e. the local scope inside foo() and global scope outside foo() .

When we print the variable inside foo() it outputs local x: 10 . This is called the local scope of the variable.

Similarly, when we print the variable outside the foo() , it outputs global x: 5 . This is called the global scope of the variable.

Nonlocal Variables

Nonlocal variables are used in nested functions whose local scope is not defined. This means that the variable can be neither in the local nor the global scope.

Let's see an example of how a nonlocal variable is used in Python.

We use nonlocal keywords to create nonlocal variables.

def outer():
    x = "local"

    def inner():
        nonlocal x
        x = "nonlocal"
        print("inner:", x)

    inner()
    print("outer:", x)


outer()

In the above code, there is a nested inner() function. We use nonlocal keywords to create a nonlocal variable. The inner() function is defined in the scope of another function outer() .

Note : If we change the value of a nonlocal variable, the changes appear in the local variable.

Built-in Functions

min()

Return the smallest item in an iterable or the smallest of two or more arguments.

# min (strings, dicts with same keys)

min([3,4,1,2]) # 1
min((1,2,3,4)) # 1
min('awesome') # 'a'
min({1:'a', 3:'c', 2:'b'}) # 1# min (strings, dicts with same keys)

max()

Return the largest item in an iterable or the largest of two or more arguments.

# max (strings, dicts with same keys)

max([3,4,1,2]) # 4
max((1,2,3,4)) # 4
max('awesome') # 'w' 
max({1:'a', 3:'c', 2:'b'}) # 3

abs()

Return the absolute value of a number. The argument may be an integer or a floating point number.

abs(-5) # 5
abs(5) # 5

sum()

  • Takes an iterable and an optional start

  • Returns the sum of start and the items of an iterable from left to right and returns the total.

  • start defaults to 0

sum([1,2,3,4]) # 10
sum([1,2,3,4], -10) # 0

zip()

  • Make an iterator that aggregates elements from each of the iterables.

  • Returns an iterator of tuples, where the i -th tuple contains the i -th element from each of the argument sequences or iterables.

  • The iterator stops when the shortest input iterable is exhausted.

first_zip = zip([1,2,3], [4,5,6])

list(first_zip) # [(1, 4), (2, 5), (3, 6)]

dict(first_zip) # {1: 4, 2: 5, 3: 6}





five_by_two = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]

list(zip(*five_by_two))

[(0, 1, 2, 3, 4), (1, 2, 3, 4, 5)]

Let's Practice

You can practice by copying the following document into your own environment.

END OF THE LECTURE

Last updated

#338:

Change request updated