Week 3 - Programming Paradigms

Learning Objectives

  • List popular programming paradigms
  • Demonstrate object oriented programming
  • Compare procedural programming and object oriented programming
  • Apply object oriented programming to solve sample problems

Computer programs and the elements they contain can be built in a variety of different ways. Several different styles, or paradigms, exist with differing popularity and usefulness for different tasks.

Some programming languages are designed to support a particular paradigm, while other languages support several different paradigms.

Three of the most commonly used paradigms are:

  • Procedural
  • Object oriented
  • Functional

Python supports each of these paradigms.

Procedural

You may not have realized it but the procedural programming paradigm is probably the approach you are currently taking with your programs.

Programs and functions are simply a series of steps to be performed.

For example:


In [67]:
primes = []
i = 2
while len(primes) < 25:
    for p in primes:
        if i % p == 0:
            break
    else:
        primes.append(i)
    i += 1

print(primes)


[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

Functional

Functional programming is based on the evaluation of mathematical functions. This is a more restricted form of function than you may have used previously - mutable data and changing state is avoided. This makes understanding how a program will behave more straightforward.

Python does support functional programming although it is not as widely used as procedural and object oriented programming. Some languages better known for supporting functional programming include Lisp, Clojure, Erlang, and Haskell.

Functions - Mathematical vs subroutines

In the general sense, functions can be thought of as simply wrappers around blocks of code. In this sense they can also be thought of as subroutines. Importantly they can be written to fetch data and change the program state independently of the function arguments.

In functional programming the output of a function should depend solely on the function arguments.

There is an extensive howto in the python documentation.

This presentation from PyCon US 2013 is also worth watching.

This presentation from PyGotham 2014 covers decorators specifically.


In [68]:
def square(val):
    print(val)
    return val ** 2

squared_numbers = [square(i) for i in range(5)]
print('Squared from list:')
print(squared_numbers)

squared_numbers = (square(i) for i in range(5))
print('Squared from iterable:')
print(squared_numbers)


0
1
2
3
4
Squared from list:
[0, 1, 4, 9, 16]
Squared from iterable:
<generator object <genexpr> at 0x7fa208be2510>

In [69]:
def squared_numbers(num):
    for i in range(num):
        yield i ** 2
    print('This is only printed after all the numbers output have been consumed')

print(squared_numbers(5))
for i in squared_numbers(5):
    print(i)


<generator object squared_numbers at 0x7fa208be2510>
0
1
4
9
16
This is only printed after all the numbers output have been consumed

In [70]:
import functools

def plus(val, n):
    return val + n


f = functools.partial(plus, 5)
f(5)


Out[70]:
10

In [71]:
def decorator(inner):
    def inner_decorator():
        print('before')
        inner()
        print('after')
    return inner_decorator

def decorated():
    print('decorated')

f = decorator(decorated)
f()


before
decorated
after

In [72]:
@decorator
def decorated():
    print('decorated')
    
decorated()


before
decorated
after

In [73]:
import time

@functools.lru_cache()
def slow_compute(n):
    time.sleep(1)
    print(n)

start = time.time()
slow_compute(1)
print(time.time() - start)

start = time.time()
slow_compute(1)
print(time.time() - start)

start = time.time()
slow_compute(2)
print(time.time() - start)


1
1.0271596908569336
0.0001976490020751953
2
1.0024516582489014

Object oriented

Object oriented programming is a paradigm that combines data with code into objects. The code can interact with and modify the data in an object. A program will be separated out into a number of different objects that interact with each other.

Object oriented programming is a widely used paradigm and a variety of different languages support it including Python, C++, Java, PHP, Ruby, and many others.

Each of these languages use slightly different syntax but the underlying design choices will be the same in each language.

Objects are things, their names often recognise this and are nouns. These might be physical things like a chair, or concepts like a number.

While procedural programs make use of global information, object oriented design forgoes this global knowledge in favor of local knowledge. Objects contain information and can do things. The information they contain are in attributes. The things they can do are in their methods (similar to functions, but attached to the object).

Finally, to achieve the objective of the program objects must interact.

We will look at the python syntax for creating objects later, first let's explore how objects might work in various scenarios.

Designing Object Oriented Programs

These are the simple building blocks for classes and objects. Just as with the other programming constructs available in python, although the language is relatively simple if used effectively they are very powerful.

Learn Python the Hard Way has a very good description of how to design a program using the object oriented programming paradigm. The linked exercise particularly is worth reading.

The best place to start is describing the problem. What are you trying to do? What are the items involved?

Example 1: A Laboratory Inventory

I would like to keep track of all the items in the laboratory so I can easily find them the next time I need them. Both equipment and consumables would be tracked. We have multiple rooms, and items can be on shelves, in refrigerators, in freezers, etc. Items can also be in boxes containing other items in all these places.

The words in bold would all be good ideas to turn into classes. Now we know some of the classes we will need we can start to think about what each of these classes should do, what the methods will be. Let's consider the consumables class:

For consumables we will need to manage their use so there should be an initial quantity and a quantity remaining that is updated every time we use some. We want to make sure that temperature sensitive consumables are always stored at the correct temperature, and that flammables are stored in a flammables cabinet etc.

The consumable class will need a number of attributes:

  • Initial quantity
  • Current quantity
  • Storage temperature
  • Flammability

The consumable class will need methods to:

  • Update the quantity remaining
  • Check for improper storage?

The consumable class might interact with the shelf, refrigerator, freezer, and/or box classes.

Reading back through our description of consumables there is reference to a flammables cabinet that was not mentioned in our initial description of the problem. This is an iterative design process so we should go back and add a flammables cabinet class.

Exercise: A Chart

We have used matplotlib several times now to generate charts. If we were to create a charting library ourselves what are the objects we would use?

I would like to plot some data on a chart. The data, as a series of points and lines, would be placed on a set of x-y axes that are numbered and labeled to accurately describe the data. There should be a grid so that values can be easily read from the chart.

What are the classes you would use to create this plot?

Pick one class and describe the methods it would have, and the other classes it might interact with.

  • One class would be a data class which would have attributes of coordinates, and type, and also sub classes of points and lines which inherited attributes, with plot method

Exercise 2: A Cookbook

A system to manage different recipes, with their ingredients, equipment needed and instructions. Recipes should be scalable to different numbers of servings with the amount of ingredients adjusted appropriately and viewable in metric and imperial units. Nutritional information should be tracked.

What are the classes you would use to create this system?

Classes:

  • ingredients
    • nutritional information
    • units
  • equipment
  • instructions
  • recipe
    • attributes
    • units
    • instructions
    • ingredients
    • units
    • nutritional information
    • prep time
    • cooking time
    • portion
    • scaling
    • name
    • equipment
    • interactions
    • cookbook
    • instructions
    • ingredients
    • equipment

Pick one class and describe the methods it would have, and the other classes it might interact with.

Building Skills in Object Oriented Design is a good resource to learn more about this process.

Syntax

Now let's look at the syntax we use to work with objects in python.

There is a tutorial in the python documentation.

Before we use an object in our program we must first define it. Just as we define a function with the def keyword, we use class to define a class. What is a class? Think of it as the template, or blueprint from which our objects will be made.

Remember that in addition to code, objects can also contain data that can change so we may have many different instances of an object. Although each may contain different data they are all formed from the same class definition.

As an example:


In [74]:
class Person(object):
    """A class definition for a person. The following attributes are supported:
    
    Attributes:
    name: A string representing the person's name.
    age: An integer representing the person's age."""

    mammal = True
    
    
    def __init__(self, name, age):
        """Return a Person object with name and age set to the values supplied"""
        self.name = name
        self.age = age


person1 = Person('Alice', 25)
person2 = Person('Bob', 30)

print(person1, person2)


<__main__.Person object at 0x7fa208c0b240> <__main__.Person object at 0x7fa208c0b128>

There is a lot happening above.

class Person(object): The class keyword begins the definition of our class. Here, we are naming the class Person. Next, (object) means that this class will inherit from the object class. This is not strictly necessary but is generally good practice. Inheritance will be discussed in greater depth next week. Finally, just as for a function definition we finish with a colon.

"""Documentation""" Next, a docstring provides important notes on usage.

mammal = True This is a class attribute. This is useful for defining data that our objects will need that is the same for all instances.

def init(self, name, age): This is a method definition. The def keyword is used just as for functions. The first parameter here is self which refers to the object this method will be part of. The double underscores around the method name signify that this is a special method. In this case the __init__ method is called when the object is first instantiated.

self.name = name A common reason to define an __init__ method is to set instance attributes. In this class, name and age are set to the values supplied.

That is all there is to this class definition. Next, we create two instances of this class. The values supplied will be passed to the __init__ method.

Printing these objects don't provide a useful description of what they are. We can improve on this with another special method.


In [75]:
class Person(object):
    """A class definition for a person. The following attributes are supported:
    
    Attributes:
    name: A string representing the person's name.
    age: An integer representing the person's age."""

    mammal = True
    
    
    def __init__(self, name, age):
        """Return a Person object with name and age set to the values supplied"""
        self.name = name
        self.age = age
    
    
    def __str__(self):
        return '{0} who is {1} years old.'.format(self.name, self.age)


person1 = Person('Alice', 25)
person2 = Person('Bob', 30)

print(person1, person2)


Alice who is 25 years old. Bob who is 30 years old.

Before we go on a note of caution is needed for class attributes. If only using strings and numbers the behaviour will likely be much as you expect. However, if using a list, dictionary, or other similar type you may get a surprise.


In [76]:
class Person(object):
    """A class definition for a person. The following attributes are supported:
    
    Attributes:
    name: A string representing the person's name.
    age: An integer representing the person's age."""

    friends = []
    
    
    def __init__(self, name, age):
        """Return a Person object with name and age set to the values supplied"""
        self.name = name
        self.age = age
    
    
    def __str__(self):
        return '{0} who is {1} years old'.format(self.name, self.age)


person1 = Person('Alice', 25)
person2 = Person('Bob', 30)

person1.friends.append('Charlie')
person2.friends.append('Danielle')

print(person1.friends, person2.friends)


['Charlie', 'Danielle'] ['Charlie', 'Danielle']

Both of our objects point to the same instance of the list type so adding a new friend to either object shows up in both.

The solution to this is creating our friends attribute only at instantiation of the object. This can be done by creating it in the __init__ method.


In [77]:
class Person(object):
    """A class definition for a person. The following attributes are supported:
    
    Attributes:
    name: A string representing the person's name.
    age: An integer representing the person's age."""

    
    
    def __init__(self, name, age):
        """Return a Person object with name and age set to the values supplied"""
        self.name = name
        self.age = age
        self.friends = []
    
    
    def __str__(self):
        return '{0} who is {1} years old'.format(self.name, self.age)


person1 = Person('Alice', 25)
person2 = Person('Bob', 30)

person1.friends.append('Charlie')
person2.friends.append('Danielle')

print(person1.friends, person2.friends)


['Charlie'] ['Danielle']

Objects have their own namespace, although we have created variables called name, age, and friends they can only be accessed in the context of the object.


In [78]:
print('This works:', person1.friends)
print('This does not work:', friends)


This works: ['Charlie']
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-78-5af80c186e3b> in <module>()
      1 print('This works:', person1.friends)
----> 2 print('This does not work:', friends)

NameError: name 'friends' is not defined

We are not limited to special methods when creating classes. Standard functions, or in this context methods, are an integral part of object oriented programming. Their definition is identical to special methods and functions outside of classes.


In [79]:
class Person(object):
    """A class definition for a person. The following attributes are supported:
    
    Attributes:
    name: A string representing the person's name.
    age: An integer representing the person's age."""

    
    
    def __init__(self, name, age):
        """Return a Person object with name and age set to the values supplied"""
        self.name = name
        self.age = age
        self.friends = []
    
    
    def __str__(self):
        """Return a string representation of the object"""
        return '{0} who is {1} years old'.format(self.name, self.age)
    
    
    def add_friend(self, friend):
        """Add a friend"""
        self.friends.append(friend)

        
person1 = Person('Alice', 25)
person2 = Person('Bob', 30)

person1.add_friend('Charlie')
person2.add_friend('Danielle')

print(person1.friends, person2.friends)


['Charlie'] ['Danielle']

Private vs Public

Some programming languages support hiding methods and attributes in an object. This can be useful to simplify the public interface someone using the class will see while still breaking up components into manageable blocks 'under-the-hood'. We will discuss designing the public interface in detail in week 5.

Python does not support private variables beyond convention. Names prefixed with a underscore are assumed to be private. This means they may be changed without warning between different versions of the package. For public attributes/methods this is highly discouraged.

Glossary

Class: Our definition, or template, for an object.

Object: An instance of a class.

Method: A function that belongs to an object

Attribute: A characteristic of an object, these can be data attributes and methods.

Exercises

Please send to me prior to class next week.

  • Pick one of the examples above (the laboratory inventory manager, the chart or the cookbook) and write out the classes it requires with their methods. You don't need to complete the code in the methods, using the pass keyword can act as a placeholder, e.g.

In [19]:
# cookbook classes


class recipe(object):
    """ A class definition of recipe. The following attributes are supported:

    Attributes:
    name: A string of the recipe name, e.g. key lime pie
    kind: A string of the type of food, e.g. dessert
    ingredients: A list of the ingredient objects required, e.g. egg, milk
    instructions: A string of the amount and steps to prepare and cook the
        ingredients, e.g. mix and bake
    equipment: A list of the equipment needed, e.g. spoon, bowl
    serving_size: An integer for the number of recommended people it can
        serve, e.g. 8
    nutrition: A dictionary of the nutritional facts, e.g. Total calories,
        fat calories
    time: A datetime for cooking time, e.g. 30 minutes"""


    def __init__(self, name, kind, ingredients, instructions, equipment,
                 serving_size, nutrition, time):
        """ Returns a recipe object with name, kind, ingredients, instructions,
        equipment, serving_size, nutrition, and time"""
        self.name = name
        self.kind = kind
        self.ingredients = ingredients
        self.instructions = instructions
        self.equipment = equipment
        self.serving_size = serving_size
        self.nutrition = nutrition
        self.time = time


    def __str__(self):
        """ Returns a basic string representation of the recipe object"""
        return "A {0} called {1}, which serves {2}.".format(
            self.kind, self.name, self.serving_size
        )


    def getNutrition(self):
        """ Returns the all nutrition facts from the ingredient objects"""
        pass


    def scaleServings(self, n):
        """ Returns scaled serving size based on n"""
        pass

pie = recipe("key lime pie", "dessert", ["pie crust", "limes"],
             "mix and bake", "bowl and knife", 4,
             {"fat": "10g", "carbs": "15g"}, 30.4)
print(pie)
print("How much fat?", pie.nutrition['fat'])


class ingredient(object):
    """ A class definition of a ingredient. The following attributes are supported:

    Attributes:
    name: A string representing the ingredient name, e.g. chicken wings
    nutrition: A dictionary representing grams in each nutrient category,
    e.g. fats, carbs, proteins, vitamins
    amount: A float in grams of the amount of ingredient proportional to
    nutritional facts, e.g. 200.0"""


    def __init__(self, name, nutrition, amount):
        """ Returns a recipe object with name, nutrition, amount"""
        self.name = name
        self.nutrition = nutrition
        self.amount = amount


    def __str__(self):
        """ Returns a basic string representation of the ingredient"""
        return "{0}, has {1} grams of carbs, and is {2} grams total.".format(
            self.name, self.nutrition["carbs"], self.amount
        )


    def scaleAmount(self, n):
        """ Returns scaled amount and nutritional facts of ingredient by n"""
        pass

egg = ingredient("egg", {"fats": 5, "carbs": 7, "proteins": 14}, 40.0)
print(egg)


A dessert called key lime pie, which serves 4.
How much fat? 10g
egg, has 7 grams of carbs, and is 40.0 grams total.
  • Add documentation to the classes and their methods

In [21]:
!flake8 cookbook.py


cookbook.py:23:5: E303 too many blank lines (2)
cookbook.py:37:5: E303 too many blank lines (2)
cookbook.py:44:5: E303 too many blank lines (2)
cookbook.py:49:5: E303 too many blank lines (2)
cookbook.py:71:5: E303 too many blank lines (2)
cookbook.py:78:5: E303 too many blank lines (2)
cookbook.py:85:5: E303 too many blank lines (2)