Sage-Code Laboratory
index<--

Python Functions

A function is a block of code that make a computation and return a result. If a block of code do not have a name or do not return a result it is not a function.This kind of block can be considered method, subroutine or maybe procedure. That is: the role of each block is establish by it's context.
continue

Function Concepts


Page Bookarks



Declaration

In Python we use keyword "def" to define a named block of code. This block can be reused to calculate one or more values called results. The result value is defined using keyword "return".If a block of code look like a function but do not return any value, then is not a function but something else.

Example:

In this example we define a block of code named "fib" that have a side-effect but does not return a result:This block of code is very similar to a function but is in fact a subprogram.That is: python do not use specific keywords like: "function", "procedure" or "method" but only "def".


# create a function fib with parameter n
def fib(n):
     """Print a Fibonacci series up to n."""
     a, b = 0, 1
     while a < n:
         print(a, end=' ')
         a, b = b, a+b
     print()
pass # end fib

fib(2000) #call function fib

Output

0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

Homework: Test this example live: Fibonacci

Note: Remember in Python the indentation is mandatory. A named block end when the indentation is back align with "def" keyword.In the example above, I have used keyword "pass" to make a "null statement" to mark the end of block.This style of programming is optional. Statement "pass" can be used to mark the end of any block of code.

Add Description

The description of a function can be created using triple quotes and string as the first statement after the function declaration. This can be used to find information about function using: help(<function_name>)

The Result

Functions usually produce a result that can be captured into a variable using assign operator "=". It can also be used in expressions. In the example below we calculate a mathematical function: "f(n) = !n" (factorial) using a recursive function call. That is a function that call itself with different parameters until a condition becomes True:

Example:


# calculate factorial of n
def factorial(n):
    if n==0:
         return 1
    else:
         return n*factorial(n-1)

# call factorial function and capture result
result=factorial(5)
print(result)

Parameters

Function parameters, sometimes called "formal parameters" are actually local variables that can receive values from a function call. Parameters can be mandatory or optional. Mandatory parameters are usually declared first. Optional parameters, have a "default value" specified in parameter declaration using notation: "param = value" or "param:type = value.

Mandatory arguments

To execute a function we use function name follow by a list of arguments enclosed in round brackets and separated by comma like: function_name(args). The parenthesis are mandatory after the function name, otherwise the function is not executed.If a function has mandatory parameters, for each parameter we must provide and argument. The argument can be a value, a variable or an expression.

Optional arguments

Arguments are paired-up with parameters by position or by name using equal sign. Mandatory and optional arguments can coexist in a call. For mandatory parameters we can assign arguments by position while optional parameters can receive arguments by name. We can also use names for all arguments, but this is unusual practice.

Example:

Next example is using what you have learned so far to create a median function. This function is good only for 2 up to 5 numbers. It also has a logical defect to establish the divisor in case of zero value arguments.


# define function with variable argument
def avg(a,b,c=0,d=0,e=0):
    divisor = 2
    if c != 0: divisor += 1;
    if d != 0: divisor += 1;
    if e != 0: divisor += 1;
    result = (a+b+c+d+e)/divisor;
    return result

# test function avg
print (avg(2,4)); # 3.0
print (avg(0,5,10)); # 5.0
print (avg(0,0,e=9)); # 3.0
print (avg(1,0,d=10,e=20)); #7.75

Homework: Copy the example from here: optional and then make a better version using variable argument "varargs", that you will learn later in this article. Post your snippet on Discord or make a Gist on GitHub and brag about it on reddit.I will give you one reputation point for this job.

Variable arguments

A procedure can receive a list of arguments into one special parameter. This feature is sometimes called: "varargs" or "rest". For declaring this parameter we use prefix "*". This parameter becomes a collection of values visible in local scope. 

Example:


# define function with variable argument
def sum_all(first:int, *rest: [int]) -> int:
    result: int = first
    for e in rest:
        result += e
    return result

# test function sum_all
print (sum_all(0)); # 0
print (sum_all(1,2)); # 3
print (sum_all(1,2,3)); # 6

# combine with spread operator
args = [1,1,1]
print (sum_all(*args)); # 3

Homework: Test this snippet on-line: Varargs

Note: In the example above I have used a new syntax available since Python 3.6, that enables you to specify parameters types, variable types and function result type. This may surprise you since you know, Python is a dynamic language. It is a good practice to use this style of programming.

Type hinting may improve program readability but does not affect performance. It was introduced to improve programming experience for developers. Python can use type hinting to avoid logical mistakes for large projects.

Namespace

A namespace is a composite word from "name" and "space". It represents a block of code that hold several identifiers. A namespace is defined by a "scope". This is a region of a program used to define: variables, constants and functions.

Example:


# define large namespace
def scope_test():
    # local scope
    def do_local():
        spam = "local spam"
    pass # end do_local

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"
    pass # end do_nonlocal

    def do_global():
        global spam
        spam = "global spam"
    pass # end do_global

    # back to namespace
    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)
pass # end scope_test

# back to global scope
scope_test() # test function with no result
print("In global scope:", spam)

Test this example live: Namespaces

Output of the program:


After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

Note: The namespaces are nested. The outermost scope is called "global" scope and it create a "global" namespace. Functions can be nested. Inside every function there is a local namespace.

Shadowing:

Using "=" will create a new variable in the local scope. If a variable exists already defined in the global scope or in the parent scope it is shadowed. We create a new variable in the local scope that hide the outer scope variable. To avoid shadowing we have to declare variables using "global" or "nonlocal" keywords.This is necessary for every single nested function that uses other variables than the local variables.

Function attributes

In Python a function is an object. Any object can have attributes that can be created using a dot operator. Function attributes are attached to the function as static variables. This is another alternative to global variables and it can be used to create encapsulated functions that behave like objects.

Example:

In the next example the function test_attr() is creating an attribute called counter the first time is called. Then the attribute is used to memorize the current counter value. Next time is able to return the value and increment it for next call.


# demonstrate attributes attached to a function
def test_attr():
    if not hasattr(test_attr, "counter"):
        test_attr.counter = 0
    else:
        test_attr.counter += 1
    return test_attr.counter
pass # end test_attr

# call function  attributes
def main():
    for i in range(0,10):
        j=test_attr()
        print(i,'->',j)
pass # end main

# compiler entry point
if __name__ == "__main__":
    main()
pass # end if

Homework: Open test this example live: Attributes

Test output:


0 -> 0
1 -> 1
2 -> 2
3 -> 3
4 -> 4
5 -> 5
6 -> 6
7 -> 7
8 -> 8
9 -> 9

Switch function:

Switch statement does not exist in Python.However you can simulate a switch statement using different techniques. In the next example we create a fake statement switch using two functions:


#define a value holder function
# => True
def switch(value):
    switch.value=value
    return True

#define matching case function
# => True or False
def case(*args):
    return any((arg == switch.value for arg in args))

# Switch example:
print("Describe a number from range:")
for n in range(0,10):
  print(n, end=",")
print(flush=True)

# Ask for a number and analyze
x=input("n:")
n=int(x)
while switch(n):
    if case(0):
        print ("n is zero;")
        break
    if case(1, 4, 9):
        print ("n is a perfect square;")
        break
    if case(2):
        print ("n is an even number;")
    if case(2, 3, 5, 7):
        print ("n is a prime number;")
        break
    if case(6, 8):
        print ("n is an even number;")
        break
    print ("Only single-digit numbers are allowed.")
pass # end of switch

Example description

switch: In the example above I use one function "switch" with attribute "value" and one function "case" that return True or False if "switch.value" is one of arguments.

case: Is a function that receive a variable number of arguments. This function uses any(...), a Python function that returns True if any item in an iterable object is True, otherwise it returns False. If the iterable object is empty, the any() function will return False. 

while: Using while loop will iterate one single time ant we can use break statement like a "switch" statement will do. This example demonstrate how using meaningful names for functions python language can be extended in interesting ways.

print: This example also demonstrate how to use print function to print numbers and avoid new line using optional parameter end=",".

Testing the program

Open this example live and run it: switch function


Describe a number from range:
0,1,2,3,4,5,6,7,8,9,
n:3
n is a prime number;

Process finished with exit code 0

Python Closures

Closure is a higher order function that encapsulate one or several other functions. The enclosed functions have access to variables used when the function was created. This is called "context".Using closures is a high appreciated skill that can improve performance of your applications.

Python can be used as a functional programming language. In Python you can create functions that will produce as a result another function. That technique is useful to create light-weight code without using object oriented programming.

Example:

Next example is a function that create another function called "test_closure"


# Create a closure
def test_closure(max):
    index = {1:-1} # mutable attribute
    def enclosed():
        if index[1] <= max:
           index[1] += 1
           return index[1]
        else:
           return -1
    return enclosed

def main():
    # use closure based function my_scan()
    my_scan = test_closure(5);
    for i in range(0,5):
        print(i,'->',my_scan())

# program entry point
if __name__ == "__main__":
    main()

Note: In python context variables are read only. However there is a workaround to help resolve this issue: You can use a dictionary or a list with a single element. The elements enclosed into any container can be read and write. In the example, index is a mutable attribute. Nice trick isn't it?

Using Closure

We use closure like a poor man class to generate function instances. This is the secret, we encapsulate one or several states with the closer function. This is helping to create similar functions at runtime based on different parameter values that can hold a state. Closures can help to avoid polluting the global scope with variables that can become hard to track.

test_closure(max)

In the example above function test_closure is a closure function because it returns a function enclosed.This function is captured using closure invocation: my_scan = test_closure(5);

my_scan()

Function my_scan is a function instance because it was created out of enclosure at runtime. Once instantiated my_scan function can be used like any other function. It is like a generator. Returns next number each time is called up to 5 then will return -1.

0 -> 0
1 -> 1
2 -> 2
3 -> 3
4 -> 4

Homework: Try to run this example live: python closure


Python Generators

The generator is a special function that do not terminate after returning a result. Instead it's instance is suspended in memory for a while until the last result is created. Think about it like a resumable function.This construct is very common in Python and is used for performance.

Example:

In this example we create a function that generate integer numbers:


#Create a generator
def test_generator(p_stop):
    counter = 0
    while counter <= p_stop:
       yield counter
       counter +=1

def main():
    # example generator:"aindex"
    print("call generator using loop")
    aindex = test_generator(3)
    for i in aindex:
        print("i=%d" % i)

    # example generator:"bindex"
    print("call generator using next")
    bindex = test_generator(3)
    print("y=%d" % next(bindex))
    print("y=%d" % next(bindex))

# compiler entry point
if __name__ == "__main__":
    main()

This will print:


call generator using loop
i=0
i=1
i=2
i=3
call generator using next
y=0
y=1

Homework: Open this example live and run it: generator

test_generator

The function generator is test_generator(). This is a resumable function that will stay in memory and wait for next() to be invoked. When the last element is generated the function terminate. A generator function is a "high order function" and must be instantiated with a parameter.

aindex

I have created 2 generators using the same function. First generator "aindex" is used into a loop like an iterable collection. Not all values are generated in memory but one by one. This is very efficient.

bindex

Second generator bindex = test_generator(3).For this generator I have used next(bindex) to create 2 values (0, 1). Evert time next() is invoked a new value is created. The third value is never used therefore "bindex" generator do not reach the end until program termination.

keyword "yield"

Keyword yield is specific to generators. This is like return, except the function do not terminate. Instead the execution is suspended and resumed using next().


Read next: Classes