7. Functions

7.1 Overview

  • Functions are the basic unit of reusable code in Python

  • if __name__ == '__main__':

    • __name__ is a special variable.

    • You can also import the functions in a code containing the above statement, but __name__ would take the name of the module, and so whatever follows this will not be executed.

    • when run as the main unit of execution, __name__ = __main__

  • All functions return a value.

    • if not specified, they return None.

7.2 Arguments

  • example: def kitten(a, b, c = 0):

    • default arguments can also be used.

    • any arguments with defaults must be after arguments without defaults.

def main():
    x = 5
    kitten(x)
    print(f'in main: x is {x}')
def kitten(a):
    a = 3
    print('Meow.')
    print(a)
if __name__ == '__main__': main()

returns

Meow.
3
in main: x is 5
  • call-by value:

    • when a value is passed to a function, the function operates on a copy of the variable. So the value is passed, not the object itself.

  • but it can get tricky.

def main():
    x = [5]
    kitten(x)
    print(f'in main: x is {x}')
def kitten(a):
    a[0] = 3
    print('Meow.')
    print(a)
if __name__ == '__main__': main()

returns

Meow.
[3]
in main: x is [3]
  • This is not call-by value(?) but call-by reference.

  • an integer is not mutable. When you assign a new value, you’re assigning an entirely new object.

  • a list is mutable, the object still points to the same list even if elements are mutated.

7.3 Variable length argument lists

  • Syntax

    def kitten(*args):
        if len(args):
            for s in args:
                print(s)
        else: 
            print('Meow.')
    
  • Traditional to name arguments, args. You can pass as many arguments as you want to the function above.

  • An interesting way to call such functions is:

    def main():
        x = ['meow', 'grrr', 'purr']
        kitten(*x)
    

7.4 Keyword arguments

  • like list arguments but dictionaries instead of tuples.

  • Syntax:

    def kitten(**kwargs):
        if len(kwargs):
            for k in kwargs:
                print('Kitten {} says {}'.format(k, kwargs[k]))
        else: 
            print('Meow.')
    
  • Call as:

    def main():
        kitten(Buffy = 'meow', Zilla = 'grr', Angel = 'rawr')
    
    • or

    def main():
        x = dict((Buffy = 'meow', Zilla = 'grr', Angel = 'rawr')
        kitten(**x)
    

7.5 Return

  • Again, no return statement => function returns None.

  • but all functions return a value.

7.6 Generator

  • instead of a single value, a generator returns a stream of values.

  • for example. range is a generator.

  • consider this example:

def inclusive_range(*args):
    numargs = len(args)
    start = 0
    step = 1
    # initialize parameters
    if numargs < 1:
        raise TypeError(f'expected at least 1 argument, got {numargs}')
    elif numargs == 1:
        stop = args[0]
    elif numargs == 2:
        (start, stop) = args 
    elif numargs == 3:
        (start, stop, step) = args
    else: raise TypeError(f'expected at most 3 arguments, got {numargs}')
    # generator
    i = start
    while i <= stop:
        yield i
        i += step
  • a good example on how to use variable length arguments.

  • yield yields a value.

  • you can call the above in this way:

def main():
    for i in inclusive_range(25):
        print(i, end = ' ')
    print()

7.7 Decorator

  • Special type of function that returns a wrapper function

  • In python, a function is also a type of object.

  • Syntax:

def elapsed_time(f):
    def wrapper():
        t1 = time.time()
        f()
        t2 = time.time()
        print(f'Elapsed time: {(t2 - t1) * 1000} ms')
    return wrapper
<>
@elapsed_time
def big_sum():
    num_list = []
    for num in (range(0, 10000)):
        num_list.append(num)
    print(f'Big sum: {sum(num_list)}')
<>
def main():
    big_sum()
  • The above wrapper can be modified for variable argument lists using *args