Functions

Dr Andy Evans

[Fullscreen]

Functions

  • So far we've been using functions from the builtin module, which is automatically loaded in most cases.
  • Occasionally we've accessed functions in other modules using import.
    import random
    a = random.random()
  • We'll now look at building our own functions.

Basic function

  • Functions are a block begun with a function declaration or header:
    def function_name ():
        # Suite of statements

    # Code calling function_name
  • In a standard script, functions need to be defined before you use them.
  • Example
    def printit():
        print("hello world")

    printit() # The function can be called
    printit() # as many times as you like.
    printit()

Passing info in

  • Functions can be parameterised; that is, made flexible through variables which are set up when the function is called.
  • Note that the arguments sent in are allocated to the parameter variables.
    def printit(text):
        print(text)

    printit("hello world")

    a = "hi you"
    printit(a)

Variable labels

  • What is happening here is we're attaching two labels to the same variable:
    def printit(text):
        print(text)

    a = "hi you"
    printit(a)
  • The effect this has depends whether the variable is mutable or immutable.
Figure showing assignment.

Variable labels

  • For mutable variables, changes inside the function change the variable outside:
    def printit(text):
        text[0] = text[0] + ", Pikachu"
        print(text[0])

    a = ["hi you"]
    printit(a) # Call the function.
    print(a) # Check the value of a.
  • As lists are mutable, the value in the list outside the method has changed to "hi you, Pikachu".

Variable labels

  • For immutable variables, changes inside the function just create a new variable inside the function. Even though it has the same name, it isn't the same variable:
    def printit(text):
        text = text + ", Pikachu" # New text variable created.
        print(text)

    a = "hi you"
    printit(a) # Call the function.
    print(a) # Check the value of a.
  • As text is immutable, the value in the list outside the method is still "hi you".

Passing info in

  • Python (unlike many languages) doesn't worry about the type passed in
    def printit(var):
        print(str(var))

    printit("hello world")
    printit(430)
    printit(430.0)
  • By default, functions invisibly return None (you can do so explicitly, as is sometimes useful).
  • But you can pass values back:
    def get_pi():
        return 3.14

    pi = get_pi()
    print(str(pi))

    print(str(get_pi()))
  • If something comes back, you either need to attach a label to it, or use it.
  • You can find out the type of a returned object with type(variable_name).
  • If you need to return more than one thing, return a tuple.

Multiple in (single out)

  • Arguments are allocated to parameters like this just on position:
    def add(num1, num2):
        return num1 + num2

    answer = add(20,30)
  • These are called positional arguments. num1 gets 20, as it is first; num2 gets 30 as it is second.

Defaults

  • You can set up default values if a parameter is missing:
    def add(num1 = 0, num2 = 0):
        return num1 + num2

    answer = add(3)
  • With this type of parameter, positional arguments are allocated left to right, so here, num1 is 3, and num2 is nothing.

Ordering

  • In the absence of a default, an argument must be passed in.
  • If we did this:
    def add(num1=0, num2):
        return num1 + num2

    answer = add(3)
    print(answer)
  • It wouldn't be clear if we wanted num1 or num2 to be 3. Because of this, parameters with defaults must come after any undefaulted parameters. All parameters to the right of a defaulted parameter must have defaults (except * and ** - which we'll see shortly).

Keyword arguments

  • You can also name arguments, these are called keyword arguments or kwargs. Note that this use of "keywords" has nothing to do with the generic use for words you can't use as identifiers.
    def add(num1, num2):
        return num1 + num2

    answer = add(num2 = 30, num1 = 50)
  • Here we've swapped the order of the positional arguments by naming the parameters to assign their values to. num2 gets 30; num1 = 50, ignoring the position.
  • Mixed arguments
    def add(num1, num2):
        return num1 + num2

    answer = add(3, num2 = 5)
  • Note that once Python has started to deal with kwargs, it won't go back and start allocating positional arguments, so all positional arguments have to be left of the kwargs.
  • You can force parameters to the right to be kwargs by putting a * in the parameters
  • Note also that you can't allocate to the same parameter twice:
    answer = add(3, num1 = 5)

Flexible parameterisation

  • You can allow for more positional arguments than you have parameters using *tuple_name, thus:
    def sum (num1, num2, *others):
        sum = num1
        sum += num2
        for num in others:
            sum += num
        return sum
    answer = sum(1,2,3,4,5,6,7)
  • * is known as the iterable un/packing operator. If nothing is allocated, the tuple is empty.

Iterable unpacking

  • You can equally use the * operator with lists or tuples to generate parameters:
    def sum (num1, num2, num3, num4):
        return num1 + num2 + num3 + num4

    a = [1,2,3,4]
    answer = sum(*a)
  • Note that these can also be in the middle.
    a = [10,20]
    answer = sum(1,*a, 2)

Iterable unpacking

  • You can, therefore, use these in both positions:
    def sum(*nums):
        sum = 0
        for num in nums:
            sum += num
        return sum

    a = [1,2,3]
    answer = sum(*a)

Flexible parameterisation

  • The same can be done with **dict_name (** is the dictionary unpacking operator), which will make a dictionary from unallocated kwargs:
    def print_details (a, **details):
        first = details["first"]
        surname = details["surname"]
        print (first + " " + surname + " has " + a + " pounds")

    print_details("5", first="George", surname="Formby")
  • Note that you can't use one of these to override other variables. If nothing is allocated, the dictionary is empty.

Unpacking dictionaries

  • You can also use ** to create keyword arguments:
    def print_details(a, first, surname):
        print (first + " " + surname + " has " + a + " pounds")

    d = {"first":"George","surname":"Formby"}
    print_details("5",**d)

Unpacking quirks

  • Note also that just as with standard arguments *tuple_name must come before **dict_name if you use both. *tuple_name must come after positional parameters and **dict_name after kwargs. It is, therefore usual to place them after their associated variables or together at the end.
    def func(a,b,*c,d,**e) # d has to be a kwarg.
    def func(a,b,d,*c,**e) # abd,bd,or d can be kwargs.
  • But this is also how you force variables to be kwargs.
  • Note that attempts to do this:
    def a(**d):
        print(d)
    a(d={1:2})
  • End up with a dictionary variable inside the dictionary, with the same name:
    {d:{1,2}}

Summary of order

  • Parameters:
    • Undefaulted parameters
    • Defaulted parameters
    • *tuple_name
    • Forced keyword parameters
    • **dict_name
  • Arguments:
    • Positional arguments
    • Keyword arguments
    • Unpacked lists, tuples, or dicts may be anywhere

Nested functions

  • Note that there is nothing to stop you having functions within functions:
    def a():
        print("1")
        def b():
            print("2")
        b()
    a()