Functions

Functions

We have used many functions in our programs: input, int, round, … . Those are built into Python and always there for us to use.

Functions

The basics again: functions are called by giving them zero or more arguments (in parens) and can return a value as their result. e.g.

len("string") - 1

Here, len is a function and "string" is an argument, and len("string") is a function call. This function call returns 6 and the expression that it's in evaluates to 5.

Functions

Functions can also not return anything, like print.

In this case, the function probably does something (like putting some characters on the screen), so you call it for that, instead of getting its result.

Defining Functions

We can define our own functions in Python. Functions are defined by a def block. Here's a simple function that doesn't return anything:

def print_one():
    """
    Print the number 1.
    """
    print("1")

This code doesn't do anything (visible) when we pass these lines of code. It defines a function (print_one) that can be used later.

Defining Functions

Somewhere later, we can call that function:

print_one()
print_one()

And the body of the function definition will run when we do.

1
1

Defining Functions

def print_one():
    """
    Print the number 1.
    """
    print("1")

The print_one function has no arguments: the () is empty.

Defining Functions

def print_one():
    """
    Print the number 1.
    """
    print("1")

The triple-quoted string after the def is the documentation string or docstring. It's a special kind of comment intended to describe (to people) what the function does: what it does, how it's used, what the arguments are.

Including it is good style, and for this course, it's mandatory on all function definitions.

Defining Functions

Here's a function that takes one argument (a string) and returns a result (also a string):

def double_word(word):
    """
    Return a doubled version of the word.
    """
    doubled = word + " " + word
    return doubled

When it's called:

dbl = double_word("hello")
print("The result: " + dbl)
The result: hello hello

Defining Functions

def double_word(word):
    """
    Return a doubled version of the word.
    """
    doubled = word + " " + word
    return doubled

The arguments act like variables that are defined by calling the function: they will take on the values given by the code that calls it.

When a function hits a return statement, it will exit (even if there's more code after that line) and give that result back to the calling code.

Defining Functions

Effectively, a function that returns something must be used as (part of) an expression. A function that doesn't return anything acts as a statement.

okayokay = double_word("okay")
print_one()

Defining Functions

The last line of code here will never run: there's no way to get to it without hitting a return first.

def absolute_value(num):
    """
    Calculate the absolute value of the number.
    """
    if num >= 0:
        return num
    else:
        return -num
    print("this never happens")

It's otherwise a correct absolute value function.

Calling Functions

We have seen functions being called: both functions we write (with def like double_word) and built-in functions (like len and print):

print(len(double_word("abc")))

Calling Functions

But what happens when you call a function? Let's look at this definition and call:

def add_one(n):
    return n + 1

print(add_one(2 * 3) + 2)

This code prints 9, but how does it get there?

Calling Functions

def add_one(n):
    return n + 1

print(add_one(2 * 3) + 2)
  1. Multiply 2*3 and get 6.
  2. Call add_one: the calling code pauses and the function starts.
  3. Inside the function, n is set to 6.
  4. The 6+1 is calculated and the function returns 7.
  5. The calling code gets the return and adds 7+2.
  6. The print prints 9.

Calling Functions

So when we call a function (defined by us or built-in), our code waits until the function is done. Then it resumes (with the return value).

Or, every function call just magically has its return value substituted into the place where the function call happened. [Both of those understandings of functions are in my head for some functions, depending on the code.]

Using Functions

Reason #1 to use functions: easy re-use of code.

Functions can be called several times to do related things. Writing a function to do that means you can write once, debug once, and use as many times as necessary.

Using Functions

e.g. in our line-intersection code, we had many lines that were almost the same:

x1 = float(input("Enter x1: "))
y1 = float(input("Enter y1: "))
x2 = float(input("Enter x2: "))
y2 = float(input("Enter y2: "))
x3 = float(input("Enter x3: "))
y3 = float(input("Enter y3: "))
x4 = float(input("Enter x4: "))
y4 = float(input("Enter y4: "))

If you're copying-and-pasting code, you almost certainly should have written a function instead.

Using Functions

The first thing I'd do: define a function to ask the user for one value and returns it, like this.

def get_coord(label):
    """
    Ask the user for one coordinate of a point.
    """
    v = float(input("Enter " + label + ": "))
    return v

That could be used eight times like this:

x1 = get_coord("x1")
y1 = get_coord("y1")
⋮

Using Functions

But its use would still be very repetitive: we have to ask for \(x\) and \(y\) values for each point. We can save that repetition:

def get_point(n):
    """
    Ask the user for an x,y pair and return it.
    """
    x = get_coord("x" + str(n))
    y = get_coord("y" + str(n))
    return x, y

Note: this function is returning two values (separated by a comma). That's possible if it's what we need to do.

Using Functions

We can get ask the user for those values and get them into variables like this:

x1, y1 = get_point(1)
x2, y2 = get_point(2)
x3, y3 = get_point(3)
x4, y4 = get_point(4)

Doing this with functions is just better code: the function names give an immediate hint what's going on, the docstrings clarity more, anything that needs to be fixed is in exactly one place (e.g. we could easily change the prompt displayed to the user in get_coord).

Variable Scope

Consider this code:

def add_double(x, y):
    dbl = 2 * y
    return x + dbl

print(add_double(2, 3))
print(dbl)

There's an error on the last line: NameError: name 'dbl' is not defined.

Variable Scope

Variables used inside a function are local variables within that function: the exist only while that function is executing and are destroyed when it returns.

That means variables inside functions are nicely separated from any outside: it's harder to affect the world outside the function, so you don't have to worry about everything else in the program while writing a function.

Variable Scope

Consider code like this function definition:

def count_to(num):
    """
    Return a string counting from 1 to num.
    """
    counting = ""
    for i in range(num):
        counting = counting + str(i + 1) + " "
    return counting

print(count_to(4))

That will print 1 2 3 4.

Variable Scope

We can use the function without worrying about reusing the same variable names it used.

num = int(input("How much counting? "))
for i in range(num):
    print(count_to(i+1))

This num is completely independent from the function's num

Variable Scope

Functions having their own local variables is a very important property of a programming language: if not, it becomes very hard to write programs that are more than a few screens of text.

Imagine a program with 100 functions, and having to scan all of them to see if anybody used the variable name num before you did.

Variable Scope

Reason #2 to use functions: logical separation of code.

Since variables are separate between functions, we should be able to write, test, and debug a function on its own, without worrying about the rest of our program. That lets us think about small pieces of code without worrying about the rest of the (possibly thousands of lines of) code.

Variable Scope

So what you should do:

When you see a relatively-independent part of your logic, move it into a function.

Whatever information you need to get that done will be the arguments to the function. Whatever result(s) it produces will be the return value(s).

Write it, test it, make sure it does what you need it to do. Then forget about it: just use it where it's needed, knowing it will do its job.

Debugging

You will make errors when writing programs.

Fixing them is a big part of the process of programming (and learning to program).

Debugging

First: don't start coding until you know what you want your program to do (and how you're going to get that done).

There's no hope of writing a correct program if you don't have a plan. You should have an algorithm in mind (or even sketched out/​written down). Then think about how to get that done in Python.

Debugging

Finding errors in a program can be very hard. The actual cause of a problem might not be anywhere close to where you notice it.

Some common categories of error, in approximate order of easiest to hardest to fix…

Debugging

Syntax errors: these occur when what you've typed just isn't legal Python.

A syntax error could be the result of inconsistent indenting in a block:

if x == 0:
    print("We're done.")
   print("Thanks.")

Debugging

Or badly matched quotes or parentheses:

print(int("34")
print("Hello world)

Or = instead of == in a condition:

if x = 0:
    print("zero")

Or many other things. Sometimes your editor's syntax highlighting will help you see syntax errors, but not always.

Debugging

Runtime errors: while running your code, some situation the language can't deal with occurs. Also called exceptions.

These can be things like a NameError where you try to use a variable name that hasn't yet been created.

Or an impossible type conversion (a ValueError):

n = int("one")

Debugging

Or division by zero (ZeroDivisionError):

q = 10 / 0

Or trying to extract part of a string that doesn't exist (IndexError):

s = "abc"
print(s[4])

Debugging

It's possible to catch these exceptions and handle them gracefully (later). For now, if something like this is unavoidable in our code (like bad user input), we'll have to let the exception occur.

Debugging

The worst category of error we're going to have to deal with: semantic errors.

This is occurs when your program runs and does something but it's not what you want/​expect.

These are hard to find because there's no error message to guide you, and many possibilities for the true cause. It could be because of an incorrect algorithm or incorrect translation of algorithm to code.

Debugging

First strategy: write your program in small chunks and test as you go.

Add print statements as you're working to inspect whatever variables you are working with. Try enough different things to convince yourself they are being set to the values you need/​expect.

If not, the problem should be in your most recent addition.

Debugging

From that last slide: …the values you need/​expect.

You can't evaluate that if you don't know exactly what you want your program to be doing at that point. Have a plan before you start typing.

Debugging

Writing modestly-sized functions can help: if there's a problem with the function, the cause is likely in the function's code, not somewhere else in the program.

It's a decent strategy to write building-blocks of your solution in functions (and test as you go), then assemble them by calling as needed.

Debugging

e.g. in assignment 1, you could have (but certainly weren't required to) write a function to get the user's input that disassembled the date string, did the integer conversions, like this:

def get_date():
    ⋮
    return y, m, d

Convince yourself it works and then call it in exactly one place:

year, month, day = get_date()

Debugging

If you realize there's a problem somewhere in your program, it can be much harder to find.

First: what's the symptom? Variable with the wrong value? An if not taken when it should be? Loop exiting too soon?

Debugging

Probably start by adding print statements to figure out what's going on. Try to narrow down where the behaviour diverges from what you need done.

Work your way back through the program until you find the cause.

Coding Style

Writing code that someone (possibly including you a week in the future) can actually read and understand is important.

Most of the expense of most programs is in maintenance (adding feature, fixing bugs). That generally involves someone else (or you in the future) going through code to figure out what's going on.

There are no fixed rules, but a few guidelines.

Coding Style

Choose descriptive variable names. Names should describe what the variable is holding: counter, total, year, user_name.

Avoid names that are too short to be meaningful (c, t, y, n) unless it's very clear from the context, or names that don't give a good hint about the value's role (stuff, things).

Coding Style

The same advice for functions names: choose get_date() or input_y_m_d() over inp().

Also break your logic up into reasonably-sized functions. Opinions vary, but if I write more than ≈20 lines of code in a straight line, I start to wonder if it could be broken into functions in some sensible way.

Coding Style

Spaces around operators aren't meaningful, so use spacing to make lines more readable.

total=a+b/factor
total = a+b / factor
total = a + b / factor
total = a + b/factor

And insert blank lines to break code up into logical chunks.

Coding Style

If all of that fails to make the code easy to understand, use comments to describe what's happening.

Comments: everything after a # on a line is ignored by Python, so you can write human-readable text there. Think of comments as a way to give a hint about why code is doing what it's doing, not about what it's doing: that should be clear to anyone who knows the language.

Coding Style

e.g. in this code, I thought initializing total to zero was obvious enough (so I didn't comment), but the reason for the initial value for val wasn't clear.

total = 0.0
val = 1.0         # dummy value so we enter the loop
while val != 0.0:

Coding Style

My usual strategy: try to make beautiful, readable code using any other combination of good coding practices. If I can't for some reason, I leave a comment explaining what's happening.

Except function docstrings: those are a specialized type of comment. I always write them and so should you (and they're required for this course).

Modules

We have seen functions that are built into the Python language (input, int, round). We have also seen how to define our own (with def).

But there are many common tasks that don't have built-in functions. We don't want to have to solve every problem by hand forever. There are lots of problems others have already solved: we'd like to use their solutions.

Modules

In Python, this other code that we can use in our program is broken up into modules.

The language comes with a bunch of commonly-useful modules: the standard library.

Modules

The built-in functions (int, print) are available in every program because they're very commonly needed. There's enough in the standard library that it doesn't make sense to have all them there all the time: there would just be too many functions hanging around.

If we want to use any of these modules, they need to be imported into our program.

Modules

e.g. the tkinter module contains functions to create graphical user interfaces: it's what IDLE uses to draw its windows on the screen. It's complex: not where we should start.

The math module contains a bunch of functions for math-related calculations. We can import it (typically at the start of the .py file) so it can be used anywhere in the program:

import math

Modules

Then the sin function to calculate the sine of an angle, or the gcd function from the math module to calculate a greatest common divisor:

s = math.sin(3.14)
g = math.gcd(18, 30)
print(s, g)
0.0015926529164868282 6

The contents of a module (when imported that way) can be accessed with a period, like modulename.function(args).

Modules

Or the time module has some tools to work with dates and times. For example, we can ask it for the current time.

import time

now = time.localtime()
print(now)

The result is a value (unlike others we have seen before) that contains some representation of the date.

time.struct_time(tm_year=2024, tm_mon=6, tm_mday=7, tm_hour=8, tm_min=41, tm_sec=39, tm_wday=4, tm_yday=159, tm_isdst=1)

Modules

It also has functions to work with those date/time objects, so we can turn it into output in a format we'd like:

now = time.localtime()
date_string = time.strftime("%Y-%m-%d", now)
print(date_string)

The "%Y-%m-%d" format string gives us a year-month-day result:

2024-06-07

Modules

There are also some tools (monotonic) that can be used to capture the time some code takes, like this:

start_time = time.monotonic()
result = calculate_something()
end_time = time.monotonic()
print("Time taken: " + str(end_time - start_time) + "s")

That will output something like:

Time taken: 0.4138080640695989s

Modules

Another example: the random module with tools to generate random values, like randint.

import random
print(random.randint(1, 100))
print(random.randint(1, 100))
print(random.randint(1, 100))
print(random.randint(1, 100))

One time I ran it, I got:

52
43
32
48

Modules

There are many things in the Python standard library: they'll be pointed out if you need them in this course.

Modules

If there's something from a module that you're going to use a lot and don't want to type the modulename. all the time, you can import that name specifically.

In the previous example we did this:

import random
print(random.randint(1, 100))

It could also have been:

from random import randint
print(randint(1, 100))

Modules

There are also many many packages freely distributed for Python, generally collected at PyPI. These have to be installed before they can be used (generally with pip, which we won't be covering/​using).

e.g. Pillow for manipulating images (PNG, JPEG, etc), or Pandas for manipulating/​analyzing data.

Objects

Python has (as do many/​most modern programming languages) the concept of objects.

An object is a way to encapsulate some part of your program: all of the information (≈variables) and actions (≈functions).

Objects

Think of a real-world object like a car.

It has a certain state: how much gas/​charge it has, how many people are in it, tires are at 30% wear, top speed, is it off or on, etc.

And there are certain actions we can take with it: add gas/​charge, change the tires, drive it for 1 km, honk the horn, etc.

There are many cars: each one has its own properties but is interacted with in basically the same way.

Objects

Objects in a programming language do a similar thing. An object is a container that holds other stuff. Each kind of object is a class and is a new type in Python.

Objects have a state that represents their current status, and there are actions that you can take with them. These are represented with variable that live inside the object.

Objects

When talking about objects, the kind of object is a class and one example is an instance. (e.g. string is the class, and "abc" is an instance.)

For example, in the fractions module contains a Fraction class that can represent a fraction or rational number.

Objects

From the docs, I see that a fraction instance can be created by giving the numerator and denominator to the constructor:

import fractions

half = fractions.Fraction(1, 2)
print(half)

This class is automatically converted to a nice string when printed:

1/2

Objects

Each class can have properties or instance variables: variables that live inside the instance and represent their state. This can be visible from the outside or can be hidden from whoever is using the class.

Instances of fraction have two visible properties:

ratio = fractions.Fraction(6, 14)
print(ratio.numerator)
print(ratio.denominator)

The fraction was automatically reduced to lowest terms:

3
7

Objects

Classes can also contain methods: functions that live inside instances and operate on them.

A fraction has a method limit_denominator that returns a fraction with a similar numeric value, but small demonimator.

f = fractions.Fraction(198698242, 1823719847)
print(f)
similar = f.limit_denominator(100)
print(similar)
198698242/1823719847
6/55

Objects

Note: the method gets both its arguments (100 on the last slide) and implicitly the instance itself (f) and can use both to produce its results.

Are those really numerically-similar fractions? We can check: fractions can be converted to floating point.

print(float(f), float(similar))
0.10895217394648445 0.10909090909090909

Objects

Summary: each class is a new Python type. Instances of a class can contain variables (properties) and functions (methods). Both are accessed from the instance with a ..

The methods a class provides are intended to help you work effectively with those values.

Objects

This specific class gives us a way to do exact arithmetic on fractional values, if that's what we need. Classes can also implement basic operations (like +) in ways that make sense for them.

>>> from fractions import Fraction
>>> Fraction(1, 10) + Fraction(2, 10)
Fraction(3, 10)
>>> Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10)
True
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

Objects

We can experiment with another class given to us by a module in the standard library: the datetime module which provides classes to work with dates and times, specifically the date class.

A date object can be constructed by giving a year, month, day:

import datetime
d = datetime.date(2024, 6, 7)

Objects

Date objects print nicely and are a distinctly new type:

print(d)
print(type(d))
2024-06-07
<class 'datetime.date'>

Objects

From the docs, I see that date objects have a month property, and a weekday method that can calculate the day of week (with Monday = 0, Tuesday = 1, etc).

print(d.month)
print(d.weekday())
6
4

Objects

Dates can also be subtracted to get another type: a timedelta.

newyear = datetime.date(2024, 1, 1)
diff = d - newyear
print(diff)
print(type(diff))
print(diff.total_seconds())
158 days, 0:00:00
<class 'datetime.timedelta'>
13651200.0

Objects

No, you weren't allowed to use this for assignment 1. But it's a good lesson: there are many things you could write yourself, and many libraries that will save you the work.

For many programming tasks, the challenge is more in finding the right library, understanding how it works, reading the documentation, etc.

Pillow

Let's try some more with Pillow, the Python Imaging Library for manipulating (bitmap) images. An image is represented by Pillow as an Image object.

We need the module installed first, since it's not part of the Python standard library. The least-worst way to do that (without the command line) seems to be:

>>> import pip
>>> pip.main(["install", "pillow"])

This is a one time installation step: run it once at the interactive prompt. Don't put it in your program.

Pillow

Then we can start with an image saved in the same directory as our Python code.

from PIL import Image
img = Image.open("Vancouver_Skyline_and_Mountains.jpg")

Now we have a Pillow Image object that we can manipulate.

Pillow

It's not clear what we're supposed to do with that object.

>>> print(img)
<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=1200x674 at 0x7F88CD4C3310>

We can use any of the methods described in the Image object docs. Most of them don't immediately make a huge amount of sense, but I know what rotating and saving are:

rotated = img.rotate(45)
rotated.save("rotated.png")

Pillow

There are also some properties that tell us useful things about the image.

>>> img.width
1200
>>> img.height
674
>>> img.mode
'RGB'

Pillow

So Pillow Image is a class that can represent bitmap images.

The Image.open function can construct an instance of Image by reading a file. So can Image.new that we'll see soon.

Pillow

The instance (an object) has some properties (like .width and .height) that tell us about the image.

We can't change those properties directly for this class: the class protects its contents from modification. This will fail:

img.width = 1201

If we want to modify the image, we need to use methods it provides.

Pillow

The Image class also contains methods that work on the image.

We say .rotate that returns another Image instance (but doesn't change the existing Image), and .save which doesn't return anything, but writes a file to disk.

Pillow

An aside: it's often useful to give multiple values as a single argument. e.g. the size of an image is actually two values (width and height), or a colour might be three (red, green, blue).

This is done with tuples in Python: multiple values in parentheses. e.g. this tuple might represent the size of an image: (1920, 1080), or this might be an RGB value representing orange: (255, 127, 0).

Pillow

Or in complete code, creating a new image (the Image.new function) requires two arguments: the colour mode, and the size (as (width, height)). Setting a pixel (an image's .putpixel method) requires two: the (x, y) location, and the colour as (r, g, b).

blank = Image.new('RGB', (200, 100))
blank.putpixel((20, 10), (255, 0, 0))
blank.save("one_red_pixel.png")

Pillow

We actually already used a tuple: the function that got user input for an \(x, y\) point returns a tuple of two things (i.e. a pair).

return x, y

Pillow

Also, function or method arguments can be given in order as we usually have:

blank = Image.new("RGB", (200, 100))
blank.putpixel((20, 10), (255, 0, 0))

Or, by name so it's more explicit what's going on. I checked the Image docs to learn the argument names, and this is equivalent:

blank = Image.new(mode="RGB", size=(200, 100))
blank.putpixel(xy=(20, 10), value=(255, 0, 0))

Pillow

blank = Image.new(mode="RGB", size=(200, 100))
blank.putpixel(xy=(20, 10), value=(255, 0, 0))

With named arguments, order doesn't matter so this is also equivalent:

blank = Image.new(size=(200, 100), mode="RGB")
blank.putpixel(value=(255, 0, 0), xy=(20, 10))

Giving arguments by name can be more clear if there are many arguments, or if some are excluded (because they are optional).

Pillow

Back to this code:

blank = Image.new('RGB', (200, 100))
blank.putpixel((20, 10), (255, 0, 0))
blank.save("one_red_pixel.png")

This starts with a blank image (default colour is black but there's an optional color argument), and draws a single red pixel near the upper-left.

In Pillow, (0, 0) is the upper-left pixel and (width-1, height-1) is the lower-right.

Pillow

The methods on Image instances let us start to actually work with images. Let's draw more than one pixel on an image:

img = Image.open("Vancouver_Skyline_and_Mountains.jpg")
for x in range(100):
    img.putpixel((x, 10), (255, 0, 0))
img.save("edited.png")

This draws red pixels from x=0 to 99, ten pixels from the top.

Pillow

Drawing individual pixels is fine, but tedious and I note in the putpixel docs:

this method is relatively slow. For more extensive changes, use … the ImageDraw module instead.

Okay, let's look at the the ImageDraw module then…

Pillow

It looks like the ImageDraw object is created from an image and then modifies it:

from PIL import ImageDraw
img = Image.new("RGB", (200, 100))
draw = ImageDraw.Draw(img)

Then we can use draw to draw on img and eventally, save it.

img.save("output.png")

Pillow

Let's draw some stuff. I see draw.rectangle and draw.line that can be given a tuple of four things: (x1, y1, x2, y2).

You can specify colours to Pillow either as a RGB triple, or use any of the named colours for the web: the named colours seem easier.

Pillow

The (0, 0) pixel is the upper-left, so (10, 20) is near there, and I'll choose (150, 75) as the other corner of our rectangle.

img = Image.new("RGB", (200, 100))
draw = ImageDraw.Draw(img)
draw.rectangle((10, 20, 150, 75), outline="white",
    fill="blue", width=8)
draw.line((10, 20, 150, 75), fill="green", width=4)
draw.line((10, 75, 150, 20), fill="red", width=4)
img.save("drawn.png")

The result:

drawn rectangle

Pillow

Or we can draw some text, if we have an appropriate font file to work with. We can get Noto Sans from the Pillow source and save it in the same directory as our code. Then (according to the docs) we have to create an ImageFont object before we can draw some text.

from PIL import ImageFont
font = ImageFont.truetype("NotoSans-Regular.ttf", size=20)

Pillow

But now we have a thing we can use to get some text on our image. After some experimenting, the position seems to be the upper-left of the text.

draw.text((10, 75), "Hello world", font=font, fill="white")
img.save("drawn2.png")
drawn rectangle

Pillow

A live-coding challenge: I'd like our guessing game to produce a visualization of the range of possible secret numbers as the game progressed.