Lecture III - Building Reusable Functions

Programming with Python

Author
Affiliation

Dr. Tobias Vlćek

Kühne Logistics University Hamburg - Fall 2024

Quick Recap of the last Lecture

Slicing

  • With slicing we can get a range of elements from a sequence
  • Syntax: sequence[start:stop:step]
  • start is the index of the first element to include
  • stop is the index of the first element to exclude
  • step is the increment between indices

. . .

Tip

If left out, the step defaults to 1. Else, start defaults to 0 and stop defaults to the length of the sequence. Negative indices can be used to slice from the end of the sequence.

Comparison Operators

  • Comparison operators are used to compare two values
  • The result of a comparison is a boolean value (True or False)
  • Operators include: ==, !=, >, <, >=, <=

. . .

> Question: Is this True?

# Careful here!
one = 1
two = 1
print(one == two)
True

Control Structures

  • Control structures allow us to control the flow of execution
  • It includes conditional statements and loops
  • Conditional statements: if, elif, else
  • Loops: for and while
  • Control flow statements (in loops): continue and break

. . .

Note

The statement continue skips the rest of the current iteration and moves to the next one in a loop while the break statement exits the loop entirely.

Functions in Detail

What is a Function?

  • Functions can accept inputs (parameters) and return outputs
  • Encapsulate logic, making code easier to maintain
  • Functions can be called multiple times from different part
  • They help reduce code duplication and improve readability
# I'm a function.
type(print)
builtin_function_or_method

. . .

Important

Remember, methods are functions that are called on an object.

Some Built-in Functions already used

  • print(): Print text to console
  • input(): Read text from console
  • len(): Get the length of a sequence
  • range(): Generate a sequence of numbers
  • round(): Round a number to a specified number of decimal places
  • type(): Get the type of an object
  • int(): Convert a string to an integer
  • float(): Convert a string to a floating-point number
  • str(): Convert an object to a string

Defining a Function

  • Use the def keyword followed by the function name
  • Inside parentheses we list the inputs (parameters)
  • The code block within every function starts with a colon (:)
  • It is indented, just as the loops from the last lecture

. . .

def greet(a_parameter):
    print(f"Hello, {a_parameter}!")
greet("Students")
Hello, Students!

. . .

Tip

It is common practice to leave out one line after the definition of a function, although we will not always do that in the lecture to save space on the slides.

Comment Functions

  • It is good practice to include a comment at the top of your functions
  • If you do it with three """, it will appear in the help menu

. . .

def greet():
    """
    This function will be used later and has currently
    absolutely no use for anything.
    """
    pass # Necessary placeholder to avoid error

help(greet)
Help on function greet in module __main__:

greet()
    This function will be used later and has currently
    absolutely no use for anything.

Naming Functions (and Methods)

  • Function names should be short, but descriptive
  • Use underscores (_) instead of spaces in the names
  • Avoid using Python keywords as function names (e.g., print)
  • Try to avoid using built-in functions and methods that have a similar name (e.g., sum and len)

> Question: Which of the following is a good name for a function?

  • myfunctionthatmultipliesvalues 
  • multiply_two_values
  • multiplyTwoValues

Function Parameters

  • Parameters are variables that the function accepts
  • They allow you to pass data to the function
  • Try to name them as variables: short and meaningful
  • We can also leave them out or define several inputs!
def greet():
    print("Hello, stranger!")
greet()
Hello, stranger!

Function Arguments

  • Arguments are the actual values passed to the function
  • They replace the parameters in the function definition

. . .

> Question: What could be the correct arguments here?

def greet(university_name, lecture):
    print(f"Hello, students at the {university_name}!")
    print(f"You are in lecture {lecture}!")

# Your code here

Initializing Parameters

  • We can also initialize parameters to a default value!
  • To do this we use the = sign and provide it with a value
  • This is called a keyword argument
def greet(lecture="Programming with Python"):
    print(f"You are in lecture '{lecture}'!")

greet()
greet("Super Advanced Programming with Python")
You are in lecture 'Programming with Python'!
You are in lecture 'Super Advanced Programming with Python'!

. . .

Tip

This is especially useful when we want to avoid errors due to missing arguments!

Multiple Parameters

  • We can also have multiple parameters in a function definition
  • They are called positional arguments and are separated by commas
  • When we call them, they must be provided in the same order
  • Alternatively, we could call them by name, as for example in this function call print("h","i",sep='')

. . .

> Question: What will be printed here?

def call_parameters(parameter_a, parameter_b):
    print(parameter_a, parameter_b)

call_parameters(parameter_b="Hello", parameter_a="World")
World Hello

Function Return Values

  • Functions can return values using the return statement
  • The return statement ends the function
  • It then returns the specified value

. . .

def simple_multiplication(a,b):
    result = a*b
    return result
print(simple_multiplication(2,21))
42

. . .

def simple_multiplication(a,b):
    return a*b # even shorter!
print(simple_multiplication(2,21))
42

Access return values

  • We can also save the return value from a function in a variable
  • That way we can use it later on in the program

. . .

def simple_multiplication(a,b):
    return a*b # even shorter!

result = simple_multiplication(2,21)
print(result)
42

Returning None

  • If we don’t specify return, functions will return None
def simple_multiplication(a,b):
    result = a*b

print(simple_multiplication(2,21))
None

. . .

> Task: Come up with a function that checks whether a number is positive or negative. It returns "positive" for positive numbers and "negative" for negative numbers. If the number is zero, it returns None.

. . .

Tip

You can also use multiple return statements in a function.

Recursion

  • Recursion is a technique where a function calls itself
  • Helps to break down problems into smaller problems

. . .

def fibonacci(n): # Classical example to introduce recursion
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(6))
8

. . .

Note

Recursion can be a powerful tool, but it can also be quite tricky to get right.

Scope

Function Scope

  • Variables defined inside a function are local to that function
  • They cannot be accessed outside the function

. . .

def greet(name):
    greeting = f"Hello, {name}!"
    
print(greeting)  # This will cause an error

. . .

> Question: Any idea how to access greeting?

Global Scope

  • Variables defined outside all functions are in the global scope
  • They can be accessed from anywhere in the program

. . .

greeting = "Hello, Stranger!"
def greet(name):
   greeting = f"Hello, {name}!"
   return greeting
print(greet("Students")) # Greet students
print(greeting) # Greet ????
Hello, Students!
Hello, Stranger!

. . .

Important

We don’t change global variables inside a function! The original value can still be accessed from outside the function.

Global Keyword

  • Still, we can change the value of greeting from inside a function!
  • By using the global keyword to modify a global variable

. . .

greeting = "Hello, Stranger!"

def greet(name):
   global greeting
   greeting = f"Hello, {name}!"
   return greeting

print(greet("Students")) # Greet students
print(greeting) # Greet students again
Hello, Students!
Hello, Students!

. . .

>Question: This can be confusing. Do you think you got the idea?

Classes

Classes

  • Classes are blueprints for creating objects
  • They encapsulate data (attributes) and behavior (methods)
  • Objects are instances of classes
  • Methods are functions that are defined within a class

. . .

class Students: # Class definition
    def know_answer(self): # Method definition
        print(f"They know the answer to all questions.")

student = Students() # Object instantiation
student.know_answer()
They know the answer to all questions.

Self

  • Classes can be quite tricky at first, especially the self keyword
  • When we call self in a method, it refers to the object itself
  • It is used to access the attributes and methods of the class
  • self always needs to be included in method definitions

. . .

# This won't work as self is missing
class Students: # Class definition
    def know_answer(): # Method definition without self
        print(f"They know the answer to all questions.")

student = Students()
student.know_answer()

. . .

>Task: Try it yourself, what is the error?

Naming Classes

  • Classes can be named anything, but it is common to use the plural form of their name (e.g., People)
  • CamelCase is used for class names, and snake_case is used for method and attribute names (e.g., TallPeople)
  • Classes are usually defined in a file with the same name as their class, but with a .py extension

. . .

Question: Which of the following is a good class name? smart_student, SmartStudent, or SmartStudents

Class Attributes

  • Class attributes are attributes that are shared by all class instances
  • They are defined within the class but outside any methods

. . .

>Question: What do you think will happen here?

class Students: # Class definition
    smart = True # Class attribute

student_A = Students() # Object instantiation student_A
student_B = Students() # Object instantiation student_B

print(student_A.smart)
print(student_B.smart)
True
True

Instance Attributes

  • Instance attributes are attributes unique to each class instance
  • They are defined within the __init__ method
class Student: # Class definition
    def __init__(self, name, is_smart): # Method for initalization
        self.name = name
        self.smart = is_smart
    def knows_answer(self): # Method to be called
        if self.smart:
            print(f"{self.name} knows the answer to the question.")
        else:
            print(f"{self.name} does not know the answer to the question.")

student = Student("Buddy",False) # Note, we don't need to call self here!
student.knows_answer()
Buddy does not know the answer to the question.

Inheritance

  • Inheritance allows a class to inherit attributes and methods
  • The class that inherits is called the subclass
  • The class that is being inherited from is called the superclass

. . .

Tip

Don’t worry!  It can be quite much right now. Hang in there and soon it will get easier again!

Inheritance in Action

class Student: # Superclass
    def __init__(self, name):
        self.name = name
    def when_asked(self):
        pass

class SmartStudent(Student): # Subclass
    def when_asked(self):
        return f"{self.name} knows the answer!"
        
class LazyStudent(Student): # Subclass
    def when_asked(self):
        return f"{self.name} has to ask ChatGPT!"

>Task: Create two students. One is smart and the other one is lazy. Make sure that both students reaction to a question is printed.

Encapsulation

  • Encapsulation is the concept of bundling data (attributes) and methods (behavior) that operate on the data into a single unit (class)
  • It is a key aspect of object oriented programming (OOP)
  • It helps in organizing code and controlling access

. . .

Note

Fortunately, this is an introduction to Python, so we won’t go into details of encapsulation.

The End

  • Interested in more detail about classes and OOP?
  • Check out access modifiers, getters and setters
  • They are definitely a bit more complicated for beginners…
  • Though they are worth learning if you build complex programs

. . .

Note

And that’s it for todays lecture!
We now have covered the basics of funtions and classes. We will continue with some slightly easier topics in the next lectures.

Literature {.title}

Interesting Book to dive deeper

  • Thomas, D., & Hunt, A. (2019). The pragmatic programmer, 20th anniversary edition: Journey to mastery (Second edition). Addison-Wesley.

. . .

Tip

A fantastic textbook to understand the principles of modern software development and how to create effective software. Also available as a really good audiobook!

. . .

For more interesting literature to learn more about Python, take a look at the literature list of this course.