www.apress.com

17/08/21

Writing Functions in Python

By Paul Gerrard 


Why Write Functions?

When you write more complicated programs, you can choose to write them in long, complicated modules, but complicated modules are harder to write and difficult to understand. A better approach is to modularize a complicated program into smaller, simpler, more focused modules and functions.

The main motivation for splitting large programs into modules and functions is to better manage the complexity of the process. 

  • Modularization “divides and conquers” the complexity into smaller chunks of less complex code, so design is easier.
  • Functions that do one thing well are easier to understand and can be very useful to you and other programmers.
  • Functions can often be reused in different parts of a system to avoid duplicating code.
  • If you want to change some behavior, if it’s in a function, you only need to change code in one place.
  • Smaller functions are easier to test, debug, and get working.

Importantly, if you choose to use a function written by someone else, you shouldn’t need to worry too much how it works, but you need to trust it. All open source or free-to-use libraries come with a health warning, but if you see many references to a library on programmer web sites and in books, you can be reasonably confident that it works.

What Is a Function?

A function is a piece of program code that is:

  • A self-contained coherent piece of functionality.
  • Callable by other programs and modules.
  • Passed data using arguments (if required) by the calling module.
  • Capable of returning results to its caller (if required).

You most likely already know about quite a few built-in Python functions. One of these is the len() function. We just call len() and pass a sequence as a parameter. We don’t need to write our own len() function, but suppose we did write one of our own (for lists and dictionaries only). It might look something like this:

        >>> def lenDictList(seq):

        ...    if type(seq) not in [list,dict]: # a seq?

        ...         return -1                   # no - fail!

        ...    nelems=0                         # length zero

        ...    for elem in seq:                 # for elem

        ...       nelems+=1                     # add one

        ...

        ... return nelems                       # length

        ...


The header line has a distinct format:

  • The keyword def to signify it is a new function.
  • A function name lenDictList (that meets the variable naming rules).
  • Braces to enclose the arguments (none, 1 or more).
  • A colon to denote the end of the header line.

The code inside the function is indented. The code uses the arguments in the function definitions and does not need to define them (they will be passed by the calling module). Here are some examples:

        >>> l = [1,2,3,4,5]

        >>> d = {1:'one',2:'two',3:'three'}

        >>> lenDictList(l)

        5

        >>> lenDictList(d)

        3

        >>> lenDictList(34)

        -1


Note that the real len() handles any sequence including tuples and strings and does better error-handling. This is a much oversimplified version.

Return Values

The results of the function are returned to the caller using the return statement. In the preceding example, there is one return value: the length of the list or dictionary provided or it is –1 if the argument is neither.

Some functions do not return a result; they simply exit.

It is for the programmer to choose how to design his or her functions. Here are some example return statements.



return
# does not return a value

return True

# True – perhaps success?

return False

# False – perhaps a failure?

return r1, r2, r3

# returns three results

return dict(a=v1,b=v2)
# returns a dictionary

Calling a Function

Functions are called by using their name and adding parentheses enclosing the variables or values to be passed as arguments. You know len() already. The other functions are invented to illustrate how functions are used.

        >>> count = len(seq)      # length of a sequence

        >>> 

        >>> # the call below returns three results, the

        >>> # maximum, the minimum, and the average of

        >>> # the numbers in a list

        >>> max, min, average = analyse(numlist)

        >>> 

        >>> # the next call provides three parameters

        >>> # and the function calculates a fee

        >>> fee = calculateFee(hours, rate, taxfactor)


Note: The number of variables on the left of the assignment must match the number of return values provided by the function.

Named Arguments

If a function just has a single argument, then you might not worry what its name is in a function call. Sometimes, though, not all arguments are required to be provided and they can take a default value. In this case you don’t have to provide a value for the argument. If you do name some arguments in the function call, then you must provide the named arguments after the unnamed arguments. Here is an example:



def fn(a, b, c=1.0):

return a*b*c

fn(1,2,3)

# 1*2*3 = 6

fn(1,2)
# 1*2*1 = 2 – c=default 1.0
fn(1,b=2)
# 1*2*1 = 2 – same result
fn(a=1,b=2,c=3)
# 1*2*3 = 6 - as before
fn(1,b=2,3)
# error! You must provide
# named args *after* unnamed
# args

Note: In your code, you must define a function before you can call it. A function call must not appear earlier in the code than the definition of that function or you will get an “undefined” error.

Variable Scope

The variables that are defined and used inside a function are not visible or usable to other functions or code outside the function. However, if you define a variable in a module and call a function inside that module, then that variable is available in the called function. If a variable is defined outside all the functions in a module, the variable is available to all of the functions in the module. For example:

        sharedvar="I'm sharable"   # a var shared by both

                                                         # functions

        def first():

           print(sharedvar)                 # this is OK

           firstvar='Not shared'         # this is unique to first

           return

        def second():

           print(sharedvar)                # this is OK

           print(firstvar)                     # this would fail!

           return


Sometimes it is convenient to create shared variables that save you time and the hassle of adding them as arguments to the functions in a module. If you use these variables as places to pass data between functions, though, you might find problems that are hard to diagnose. Treating them as readonly variables will reduce the chance of problems that are hard to debug.


About the Author

Paul Gerrard is a consultant, teacher, author, webmaster, programmer, tester, conference speaker, rowing coach, and publisher. He has conducted consulting assignments in all aspects of software testing and quality assurance, specializing in test assurance. He has presented keynote talks and tutorials at testing conferences across Europe, the United States, Australia, and South Africa, and he has occasionally won awards for them. He has been programming since the mid-1970s and loves using the Python programming language.

Want more? This article is excerpted from Lean Python (2016).  Get your copy today and learn the essential aspects of Python, focusing on features you’ll use the most.