Lec04-Functions (New)
Lec04-Functions (New)
Computer
Principles and
    Python
Programming
Lecture 4: Functions
  2024-25 Term 2
 By Dr. King Tin Lam
Outline
• Defining functions
• The return statement
• Parameter passing
  • Positional arguments
  • Keyword arguments
  • Arbitrary arguments
• Function annotations
• Inner functions
• Lambda expressions (preview)
• Variable scope and lifetime
                                 2
     Why do we use functions in programming?
     • Make an application or system modular!
     • Modular programming - separating the functionality of a program
       into independent code modules; functions can be seen a kind of such
       modules
     • Advantages:
            •   Code reusability
            •   Easier to debug or maintain (extend) the program
            •   Facilitate software testing
            •   Code optimization: write less code
   fun1()
   fun2()                                  program
                                                                             3
script                script (modular)
Why Do We Use Functions in Programming?
• So far, we write program code as sequential     a, b = 0, 1
                                                  while a < 100:
  lines in the body of the main module.               print(a, end=' ')
                                                      a, b = b, a+b
• Suppose you need to print three Fibonacci       print()
  sequences from 0 to 100, from 0 to 500, and     a, b = 0, 1
  from 0 to 2000, would you write the code on     while a < 500:
  the right?                                          print(a, end=' ')
                                                      a, b = b, a+b
• It looks very redundant and clunky!             print()
                                                  a, b = 0, 1
• A better solution is to move your code into a   while a < 2000:
  function and generalize it for future reuse.        print(a, end=' ')
                                                      a, b = b, a+b
                                                  print()
                                                                      4
Why Do We Use Functions in Programming?
                                parameter
• Write once:   def fib(n):
                    """Print a Fibonacci series up to n."""
                    a, b = 0, 1
                    while a < n:
                        print(a, end=' ')
argument
                        a, b = b, a+b
                    print()
                                       …
                                                                        5
Types of Functions
• Built-in functions - Functions that are built into Python.
• User-defined functions - Functions defined by the programmer.
                                                                  6
Built-in Functions
• The Python interpreter has a number of functions and types built into it that are
  always available. They are listed here in alphabetical order.
     abs()           delattr()     hash()         memoryview()   set()
     all()           dict()        help()         min()          setattr()
     any()           dir()         hex()          next()         slice()
     ascii()         divmod()      id()           object()       sorted()
     bin()           enumerate()   input()        oct()          staticmethod()
     bool()          eval()        int()          open()         str()
     breakpoint()    exec()        isinstance()   ord()          sum()
     bytearray()     filter()      issubclass()   pow()          super()
     bytes()         float()       iter()         print()        tuple()
     callable()      format()      len()          property()     type()
     chr()           frozenset()   list()         range()        vars()
     classmethod()   getattr()     locals()       repr()         zip()
     compile()       globals()     map()          reversed()     __import__()
     complex()       hasattr()     max()          round()                             7
Example: The round() Function
                                  have two parameters
• round(number[, ndigits])
• Return number rounded to ndigits precision after the decimal
  point. If ndigits is omitted or is None, it returns the nearest integer
  to its input.
              >>> round(3.4)            >>> round(2.4)
              3                         2
              >>> round(3.5)            >>> round(2.5)
              4                         2                   Not 3 but 2!
• The last result is not a bug but a "new norm" (a change since Python
  3) which rounds half-way cases to the nearest even number. This is
  so-called "round half to even" or "banker's rounding".
                                                                            8
                                                                            Detour
• Check the doc: This is not a bug: it’s a result of the fact that most
  decimal fractions can’t be represented exactly as a float.
                                                                                  9
                                                                       Detour
                                                                            10
Defining Functions
• A function is a self-contained block of one or more statements that
  performs a special task when called.
• Syntax:        def name_of_function(parameters):     function header
                   statement1
                   statement2
                   statement3                         function body
                   ...
                   statementN
                                                                         11
Defining a Function
• Example:
                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()
                                                              12
Control Flow Across Function Calls
                                                           print("At the beginning ...")
• You can assign the function object to another variable and call it:
        >>> f = fib
        >>> f
        <function fib at 0x7f91de268e50>
        >>> f(2000)
        >>> 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
                                                                                        14
Defining Functions
• The last example is a void function, which doesn't return a value.
• You can define a function that returns a value.
    def fib2(n):
        """Return a list containing the Fibonacci series up to n."""
        result = []
        a, b = 0, 1
        while a < n:
            result.append(a)
            a, b = b, a+b
        return result
Defining Functions
• The statement result.append(a) calls a method of the list object
  result. A method is a function that belongs to an object and is called
  via the syntax obj.methodname, where obj is an object (reference),
  and methodname is the name of a method defined by the object’s
  type. More details will be given when teaching OOP.
    def fib2(n):
        """Return a list containing the Fibonacci series up to n."""
        result = []
        a, b = 0, 1
        while a < n:
            result.append(a)
            a, b = b, a+b
        return result
                                                                           16
The return Statement
• The return statement is used to exit a function and go back to the
  place from where it was called.
• Syntax:           return [expression_list]
            They are the same; no behavioral difference; all return None to the caller.
                                                                                              17
The return Statement                            Indeed, there is a built-in function
                                                also called abs() in Python. We
                                                wrote this function just as an example
                                                of returning a conditional expression.
• Example: returning an expression
           def abs(num):
               """Return the absolute value of a number."""
               print("absolute value")
               return num if num >= 0 else -num
           x = abs(-1)
           print(x)
           print(abs(2.1))     absolute
                               1
                     Output:   absolute
                               2.1
                                                                                   18
Returning Multiple Values
• It is also possible for a function to return multiple values and assign
  the returned multiple values to multiple variables.
• Example:
 def search(lst, target):
     for index, val in enumerate(lst, start=1):
         if val == target:
             return index, val
     return None, "Not found"
                                                                        20
Multiple Return Statements
• How about this version?
                     def is_divisible(a, b):
                         if not a % b:
                             return True
                         else:
                             return False                    You will never reach this
                         return None                         statement – pointless to
                                                             put it here.
• The correct and concise version:
                     def is_divisible(a, b):
                         if not a % b:
                                           return not a%b
                             return True   or
                         return False      return a%b == 0
                                                                                     21
Default Argument Values
• You can specify a default value for one or more arguments.
• Example:
               def ask_ok(prompt, retries=4, reminder='Please try again!'):
                   while True:
                       ok = input(prompt + ' ')
                       if ok in ('y', 'ye', 'yes'):
                           return True
                       if ok in ('n', 'no', 'nop', 'nope'):
                           return False
                       retries = retries - 1
                       if retries < 0:
                           raise ValueError('invalid user response')
                       print(reminder)
https://docs.python.org/3/tutorial/controlflow.html#default-argument-values   22
Default Argument Values
• The function can be called in several ways:
   1. Giving only the mandatory argument:
                   ans = ask_ok('Are you sure to quit?')
                                                                      23
Default Argument Values
• Sample outputs:         >>> ans = ask_ok('Are you sure to quit?', 2)
                          Are you sure to quit? I
                          Please try again!
                          Are you sure to quit? am
                          Please try again!
                          Are you sure to quit? stupid
                          Traceback (most recent call last):
                            File "<stdin>", line 1, in <module>
                            File "<stdin>", line 10, in ask_ok
                          ValueError: invalid user response
                     def func(arg=default):
                         print(arg)
                     default = 3                       Output:
                     func()                             2
• Note: default values are evaluated only once at the point of function
  definition in the defining scope.
                                                                           25
Default Argument Values
• Compare outputs of the following code segments?
         def f(a, L=[]):                def f(a, L=None):
             L.append(a)                    if L is None:
             return L                           L = [] L is set to None when the function
                                            L.append(a) definition time
         print(f(1))       [1]              return L
                                                Output:
         print(f(2))       [1, 2]
         print(f(3))       [1, 2, 3]             2
                                        print(f(1))        [1]
                                        print(f(2))              [2]
                                        print(f(3))              [3]
• Since default values are evaluated only once, this makes a difference
  when the default is a mutable object such as a list, dictionary, or
  instances of most classes.
                                                                                   26
Default Argument Values
• Try this out:
                       def display(code=123, mesg):
                           pass
                                                                     27
Keyword Arguments
• Revisit the built-in print() function that you use often!
• In fact, it accepts these optional attributes sep, end, file, and flush
  as keyword arguments.
• Examples:
               >>> print('he', 'she', 'it')
               he she it
               >>> print('he', 'she', 'it', sep=', ')
               he, she, it
                                                                        28
Keyword Arguments
• Functions can also be called using keyword arguments.
• Syntax to call a function using keyword argument is:
     function_name(pos_args, keyword1=value, keyword2=value2, …)
                                                                   29
   Precautions of Using Keyword Arguments
   • For this function:    def display(name, action="greet", mesg="Hello,"):
                               if action == "greet":
                                   print(mesg, name)
                               elif action == "punch":
                                   print("Take this!", name)
                                                                                        30
Precautions of Using Keyword Arguments
• For this function:   def display(name, action="greet", mesg="Hello,"):
                           ...
                                                                    32
Arbitrary Argument Lists
                                                     list[1,2,3]
                                                     tuple(1,2,3)
• A function in Python can also be defined to receive an arbitrary
  number of arguments, which are wrapped up in a tuple (a read-only
  list of items) when being passed from the caller.
• Syntax:      def func(*args):
   • Zero or more normal arguments can be defined before the variable number
     of arguments.
   • Normally, these variadic arguments will be last in the list of formal
     parameters, because they scoop up all remaining input arguments passed to
     the function. Any formal parameters which occur after the *args parameter
     are keyword-only arguments.
                                                                                 33
Arbitrary Argument Lists
                                         Syntax: str_name.join(iterable)
• Examples:                              The join() method returns a string concatenating
                                         items of the iterable with str_name as separator.
                                                                                     34
Unpacking Argument Lists
• In some reverse situation, if the actual arguments are already in a list
  or tuple but your function expects separate positional arguments,
  then you need to "unpack" the list or tuple first using the * operator.
• Examples:           def sum_of_4(a, b, c, d):
                            return a + b + c + d
>>> x = [1, 2, 3, 4]
                                                                                    35
Function Annotations (Since Python 3.5)
• Function annotations are optional metadata information about the
  types used by user-defined functions (see PEP 3107 and PEP 484).
• Annotations are stored in the __annotations__ attribute of the
  function as a dictionary and have no effect on any other part of the
  function.
• Parameter annotations are defined by a colon after the parameter
  name, followed by an expression evaluating to the value of the
  annotation.
• Return annotations are defined by a literal ->, followed by an
  expression, between the parameter list and the colon denoting the
  end of the def statement.
                                                                         36
Function Annotations                                                               default argument
 Incorrect syntax:    def movie_info(title: str, year: int, rating = 3.5 : float) -> str:
                     Putting the default argument before the parameter annotation is incorrect.
                                                                                                                        37
                                                                             (Optional)
Function Annotations
• Note that Python doesn’t provide any semantics with annotations. It
  only provides nice syntactic support for associating metadata.
   • Typing is not necessarily the only one purpose of adding annotations.
• So, you may also write annotations like below (but the type checkers
  in some IDEs may highlight them even though the code can run):
 def movie_info(t: 'movie title (str)', y: 'release year (int)',
                rating: 'between 0 and 5 (float)' = 3.5) -> 'formatted string':
     print("Annotations:", movie_info.__annotations__)
     return f"'{t}' released in {y} is rated {rating:.1f} stars."
Type Hints
• The typing module provides runtime support for type hints.
• The most fundamental support consists of the types:
   •   Any - Special type indicating an unconstrained type.
   •   Union - Union type; Union[X, Y], equiv. X | Y, means either X or Y.
   •   Callable - Callable type
   •   TypeVar - Type variable
   •   Generic - Abstract base class for generic types
• For a full specification, please see PEP 484.
• For a simplified introduction to type hints, see PEP 483.
                                                                              39
                                                                                           (Optional)
Type Hints
• For example, to annotate a function f that accepts an integer and
  returns either a list of integers or a string, write: return value is either list of int or str
                      def f(n: int) -> list[int] | str:                (PEP 604, since 3.10)
  or
                      from typing import Union
                                                                                                     40
Unix Philosophy: Do one thing and do it
well!
• What do you think about this function?
    def print_twins(start, end):
        assert start > 1
        for n in range(start, end-1):
            is_prime1 = True
            for i in range(2, n):
                if n % i == 0:                  These two parts look redundant.
                    is_prime1 = False
            if is_prime1:
                is_prime2 = True                Implication:
                for i in range(2, n+2):         This function can be decomposed further.
                    if (n + 2) % i == 0:
                        is_prime2 = False
                if (is_prime1 and is_prime2):
                    print(f"({n}, {n+2})")                                        41
Unix Philosophy: Do one thing and do it
well!
• This one looks much better – every function does one thing only.
• Better chance for code reuse
              def is_prime(n):
                  for i in range(2, n):
                      if n % i == 0:
                          return False
                  return True
                               print_twins(2, 100)                         43
Lambda Expressions
• When we create a function in Python, we use the def keyword and
  give the function a name.
• Lambda expressions are used to create anonymous functions (i.e.,
  those without a name), allowing us to define functions more quickly.
• A lambda expression yields a function object.
• Syntax:
                                                 def func_obj(arguments):
 func_obj = lambda arguments: expression
                                                     return expression
• Examples:
                                           def add(x, y):
         add = lambda x, y: x + y
                                               return x + y
                                                                            44
Lambda Expressions
• Lambdas differ from normal Python functions because they can have
  only one expression, can't contain any statements and their return
  type is a function object.
                            lambda x, y: x + y
• So, the line of code above doesn't exactly return the value x + y but
  the function that calculates x + y.
                                                                          45
Lambda Expressions
• Examples:          # define a usual function
                     def cube(y):
                         return y*y*y
                                                             46
Scope and Lifetime of Variables
                                  47
Scope and Lifetime of Variables
• A variable is only available from inside the region it is created. This is
  called scope.
                def my_func():
                    x = 123                  x is only inside the function
                    print(x)          123
                my_func()
                print(x)              NameError: name 'x' is not defined
                                                                               48
Scope and Lifetime of Variables
• Global Scope - a variable created in the main body of the Python
  code is a global variable and belongs to the global scope.
• Global variables are available from within any scope, global and local.
                      x = 123
                      def my_func():
                          print(x)
my_func() 123
print(x) 123
                                                                        49
Scope and Lifetime of Variables
• Let's consider the case with an inner function!
• The variable x is not available outside my_func(), but it is available for
  any function inside its body.
                   def my_func():
                       x = 246
                       def my_inner_func():
                           print(x)              246
                       my_inner_func()
my_func()
                                                                           50
Scope and Lifetime of Variables
• How about this code? What is the output?
                               local variable, just use it inside the function
        def my_func(lst):
            for i, val in enumerate(lst, 1):                                1    a
                print(i, val)                                               2    b
                                                                            3    c
            print("# total items =", i)                                     4    d
            print(val)                                                      5    e
                                                                            #    total items = 5
        my_func(["a", "b", "c", "d", "e"])                                  e
• No error above! Python does not have block scopes as C/C++ do.
• So, variables i and val are visible for the rest of the function body once
  they are created (even created in the for loop).
                                                                                                   51
Scope and Lifetime of Variables
• But soon later, you will learn a technique called list comprehension:
      def my_func(lst):
          tmp = [val for val in lst]
          print(val)
                            NameError: name 'val' is not defined
• In this case (and in Python 3), the variable val is not visible outside
  the brackets.
• The same principle applies to comprehensions of other types such as
  dict and set.
                                                                            52
Scope and Lifetime of Variables
• How about this code?     x = 2
                           my_func()                3
                           print(x)                 2
• If you operate with the same variable name inside and outside of a
  function, Python will treat them as two separate variables, one in the
  global scope and one in the local scope.
• This is an example of variable shadowing (or name masking).
                                                                       53
Scope and Lifetime of Variables
• Another example (nested):
               x = 0
               def outer():
                   x = 1
                   def inner():
                       x = 2
                       print("inner:", x)
                   inner()
                   print("outer:", x)
                                            inner: 2
               outer()                      outer: 1
               print("global:", x)          global: 0
                                                        54
Scope and Lifetime of Variables
• As there is no variable          x = 0
  declaration but only variable    def outer():
  assignment in Python, the            x = 1
  keyword nonlocal introduced
                                       def inner():
  in Python 3 is used to avoid             nonlocal x
  variable shadowing and                   x = 2
  assign to non-local variables.           print("inner:", x)   inner: 2
                                                                outer: 2
                                       inner()                  global: 0
                                       print("outer:", x)
                                   outer()
                                   print("global:", x)
                                                                    55
Scope and Lifetime of Variables
                                                               If you add nonlocal keyword
                                       x = 0                   to the x here, you will get
• In this example, accessing the                               SyntaxError: no binding for
                                       def outer():
  variable x defined in outer() from       x = 1               nonlocal 'x' found. Nonlocal
  the body of innermost() requires                             bindings can only be used
                                          def inner():         inside nested functions.
  two nonlocal bindings to reach x.           nonlocal x
• In fact, a nonlocal statement               x = 2
                                              y = 3.14
  allows multiple variables. In this
  example, both x and y are nonlocal           def innermost():
  variables in innermost().                        nonlocal x, y
                                                   x = 3; y *= 2
• Note that nonlocal bindings can                  print("innermost:", x)
  only be used inside nested                   innermost()
  functions. You can't use nonlocal            print("inner:", x, y)
  to refer to a global variable                                           innermost: 3
  defined outside the outer()             inner()                         inner: 3 6.28
                                                                          outer: 3
                                          print("outer:", x)
  function.                                                               global: 0
                                       outer()
                                                                                     56
                                       print("global:", x)
Scope and Lifetime of Variables
• The keyword global is used to   x = 0
  avoid variable shadowing and    def outer():
  assign to global variables.         x = 1
                                      def inner():
                                          global x
                                          x = 2
                                          print("inner:", x)   inner: 2
                                                               outer: 1
                                      inner()                  global: 2
                                      print("outer:", x)
                                  outer()
                                  print("global:", x)
                                                                   57
Scope and Lifetime of Variables
• This code is a bit trickier.                 def outer():
                                                   x = "Old" inner is still the old
• The global statement inside the inner        ❌   def inner():
  function does not affect the variable x              global x
                                                       x = "New"
  defined in the outer function, which             print("Before inner:", x)
  keeps its value 'Old'.                           inner()
                                                   print("After inner: ", x)
• After calling inner(), a variable x exists
  in the module namespace and has the          outer()
                                               print("In main:", x)
  value 'New'. It can be printed at the
  module level or main (i.e. outside the                       Before inner: Old
                                                               After inner: Old
  functions).                                                  In main: New
                                                                                58
Programming Advice
• Global variables are generally bad practice and should be avoided.
   • It is okay to define some global “constants” (i.e., just for read-only purposes)
     that may be used throughout the program.
   • But in most cases, we should pass values as arguments to a function or return
     values from it instead of reading or writing global variables.
                                                                                    59
Scope: Summary
• Python creates a new scope for each module, class, function,
  generator expression, dict comprehension, set comprehension and in
  Python 3.x also for each list comprehension.
• Apart from these, there are no nested scopes inside of functions.
60