Python Functions and Classes

This material is adapted from the Earth and Environmental Data Science, from Ryan Abernathey (Columbia University)

For longer and more complex tasks, it is important to organize your code into reuseable elements.

For example, if you find yourself cutting and pasting the same or similar lines of code over and over, you probably need to define a function to encapsulate that code and make it reusable.

Dry programming

An important principle is programming in DRY: “don’t repeat yourself”. Repetition is tedious and opens you up to errors. Strive for elegance and simplicity in your programs.

Functions

Functions are a central part of advanced python programming. Functions take some inputs (arguments) and do something in response.

Usually functions return something, but not always.

# define a function
def say_hello():
    """Return the word hello."""
    return 'Hello'
Copy to clipboard
# functions are also objects
type(say_hello)
Copy to clipboard
function
Copy to clipboard
# this doesnt call
say_hello?
Copy to clipboard
# this does
say_hello()
Copy to clipboard
'Hello'
Copy to clipboard
# assign the result to something
res = say_hello()
res
Copy to clipboard
'Hello'
Copy to clipboard
# take some arguments
def say_hello_to(name):
    """Return a greeting to `name`"""
    return 'Hello ' + name
Copy to clipboard
# intended usage
say_hello_to('World')
Copy to clipboard
'Hello World'
Copy to clipboard
say_hello_to(10)
Copy to clipboard
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-107e7297d09f> in <module>
----> 1 say_hello_to(10)

<ipython-input-6-6c2b731d9caf> in say_hello_to(name)
      2 def say_hello_to(name):
      3     """Return a greeting to `name`"""
----> 4     return 'Hello ' + name

TypeError: can only concatenate str (not "int") to str
Copy to clipboard
# redefine the function
def say_hello_to(name):
    """Return a greeting to `name`"""
    return 'Hello ' + str(name)
Copy to clipboard
say_hello_to(10)
Copy to clipboard
'Hello 10'
Copy to clipboard
# take an optional keyword argument
def say_hello_or_hola(name, spanish=False):
    """Say hello in multiple languages."""
    if spanish:
        greeting = 'Hola '
    else:
        greeting = 'Hello '
    return greeting + name
Copy to clipboard
print(say_hello_or_hola('Ryan'))
print(say_hello_or_hola('Juan', spanish=True))
Copy to clipboard
Hello Ryan
Hola Juan
Copy to clipboard
# flexible number of arguments
def say_hello_to_everyone(*args):
    return ['hello ' + str(a) for a in args]
Copy to clipboard
say_hello_to_everyone('Ana', 'Jody', 'Tristan')
Copy to clipboard
['hello Ana', 'hello Jody', 'hello Tristan']
Copy to clipboard

Pure vs. Impure Functions

Functions that don’t modify their arguments or produce any other side-effects are called pure.

Functions that modify their arguments or cause other actions to occur are called impure.

Below is an impure function.

def remove_last_from_list(input_list):
    input_list.pop()
Copy to clipboard
names = ['Ana', 'Jody', 'Tristan']
remove_last_from_list(names)
print(names)
remove_last_from_list(names)
print(names)
Copy to clipboard
['Ana', 'Jody']
['Ana']
Copy to clipboard

We can do something similar with a pure function.

Note!

In general, pure functions are safer and more reliable.

def remove_last_from_list_pure(input_list):
    new_list = input_list.copy()
    new_list.pop()
    return new_list
Copy to clipboard
names = ['Ana', 'Jody', 'Tristan']
new_names = remove_last_from_list_pure(names)
print(names)
print(new_names)
Copy to clipboard
['Ana', 'Jody', 'Tristan']
['Ana', 'Jody']
Copy to clipboard

We could spend the rest of the day talking about functions, but we have to move on.

Namespaces

In python, a namespace is a mapping between variable names and python object. You can think of it like a dictionary.

The namespace can change depending on where you are in your program. Functions can “see” the variables in the parent namespace, but they can also redefine them in a private scope.

name = 'Ana'

def print_name():
    print(name)

def print_name_v2():
    name = 'Jody'
    print(name)
    
print_name()
print_name_v2()
print(name)
Copy to clipboard
Ana
Jody
Ana
Copy to clipboard

A more complex function: Fibonacci Sequence

The Fibonacci sequence is the 1,1,2,3,5,8…, the sum of each number with the preceding one. Write a function to compute the Fibonacci sequence of length n. (Hint, use some list methods.)

def fib(n):
    l = [1,1]
    for i in range(n-2):
        l.append(l[-1] + l[-2])
    return l
Copy to clipboard
fib(10)
Copy to clipboard
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Copy to clipboard

Classes

We have worked with many different types of python objects so far: strings, lists, dictionaries, etc. These objects have different attributes and respond in different ways to the built-in functions (len, etc.)

A class to represent a cyclone

class Cyclone:
    
    def __init__(self, name):
        self.name = name
Copy to clipboard
h = Cyclone('Yasi')
h
Copy to clipboard
<__main__.Cyclone at 0x7ffbaca42eb0>
Copy to clipboard

Our class only has a single attribute so far:

h.name
Copy to clipboard
'Yasi'
Copy to clipboard

Let’s add more, along with some input validation:

class Cyclone:
    
    def __init__(self, name, category, lon):
        self.name = name.upper()
        self.category = int(category)
        
        if lon > 180 or lon < -180:
            raise ValueError(f'Invalid lon {lon}')
        self.lon = lon
        
Copy to clipboard
h = Cyclone('Yasi', 5, 143)
h
Copy to clipboard
<__main__.Cyclone at 0x7ffbb03042e0>
Copy to clipboard
h.name
Copy to clipboard
'YASI'
Copy to clipboard
h = Cyclone('Debbie', 4, 300)
Copy to clipboard
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-28-cb744dab3e2e> in <module>
----> 1 h = Cyclone('Debbie', 4, 300)

<ipython-input-25-8485b09b060f> in __init__(self, name, category, lon)
      6 
      7         if lon > 180 or lon < -180:
----> 8             raise ValueError(f'Invalid lon {lon}')
      9         self.lon = lon
     10 

ValueError: Invalid lon 300
Copy to clipboard

Now let’s add a custom method:

class Cyclone:
    
    def __init__(self, name, category, lon):
        self.name = name.upper()
        self.category = int(category)
        
        if lon > 180 or lon < -180:
            raise ValueError(f'Invalid lon {lon}')
        self.lon = lon
    
    def is_dangerous(self):
        return self.category > 1
Copy to clipboard
f = Cyclone('Yasi', 5, 143)
f.is_dangerous()
Copy to clipboard
True
Copy to clipboard

Magic / dunder methods

We can implement special methods that begin with double-underscores (i.e. “dunder” methods), which allow us to customize the behavior of our classes. (Read more here). We have already learned one: __init__. Let’s implement the __repr__ method to make our class display something pretty.

class Cyclone:
    
    def __init__(self, name, category, lon):
        self.name = name.upper()
        self.category = int(category)
        
        if lon > 180 or lon < -180:
            raise ValueError(f'Invalid lon {lon}')
        self.lon = lon
        
    def __repr__(self):
        return f"<Cyclone {self.name} (cat {self.category})>"
    
    def is_dangerous(self):
        return self.category > 1
Copy to clipboard
f = Cyclone('Yasi', 5, 143)
f
Copy to clipboard
<Cyclone YASI (cat 5)>
Copy to clipboard