The Invent with Python Blog

Wed 20 December 2017

WizMon: An Object-Oriented Programming Tutorial for Python

Posted by Al Sweigart in misc   

TODO - explain terms: attribute, class, data type, variable, method, function

Object-oriented programming (OOP) is a way of organizing your code into units called classes. It's a rather abstract concept, and explanations usually are vague, use poor metaphors, or are filled with intimidating jargon like "polymorphism" and "encapsulation".

For example, many tutorials describe a Car class, which in my opinion is a poor vehicle for explanation. (I never apologize for my puns.) After all, a "car" represented by a Car class will be very different depending on what kind of software it's for:

  • A racing video game
  • An app for a car dealership
  • A traffic flow simulator

Often, tutorials use overly simplified Car class examples, making it hard to understand what exactly classes are good for. This contributes to creating classes for the sake of creating classes. Object-oriented programming is a fantastic method to make large amounts of code manageable, but is often overkill for small programs. Python's OOP features are optional; you can write lots of code without ever needing to create classes.

Think of OOP as a bureacracy. For large organizations, the mid-level managers, processes, and paperwork of bureacracy are needed to keep things running. But for small organizations, it's can just be red tape that gets in the way of real work.

In this tutorial, we will write Python code for a WizardMoney class in a module named wizmon that manages the currency from the Harry Potter books. In the wizarding world, there are bronze knuts (worth 1 unit of money), silver sickles (worth 29 knuts), and gold galleons (worth 17 sickles or 493 knuts). Think of it as a foreign currency that only has 1-cent, 17-cent, and 493-cent coins. Exchanging these oddly-numbered denominations is difficult, so we'll write some software to help manage it.

A class isn't a complete piece of software on its own, but rather used in an application. Maybe we're creating a Harry Potter video game, or a Harry Potter website that has some currency-exchange feature. Whatever code we write, the WizardMoney class will help us manage these currency-related features of the software.

I highly recommend that you type the code out yourself into IDLE's file editor and interactive shell (or whichever text editor you use). You'll remember much more this way, rather than just reading along. The full WizardMoney class is available in the wizmon module at https://github.com/asweigart/wizmon. You can install it with pip by running pip install wizmon or pip3 install wizmon.

First, let's begin with a non-OOP example of how we would code this.

A Non-OOP Example of Wizard Money

Let's create code to manage these three denominations without using classes. We'll use dictionaries with keys 'galleons', 'sickles', and 'knuts' whose values are integers keeping track of how many of each coin there is. These dictionaries represent a certain amount of money in two piles of coins:

pileA = {'galleons': 2, 'sickles': 10, 'knuts': 5}
pileB = {'galleons': 0, 'sickles': 0, 'knuts': 25}

Also, let's make a function that generates these dictionaries for us so we don't end up using a dictionary that has missing/typo keys:

def makeWizardMoneyDict(galleons=0, sickles=0, knuts=0):
    return {'galleons': galleons, 'sickles': sickles, 'knuts': knuts}

pileA = makeWizardMoneyDict(2, 10, 5)
pileB = makeWizardMoneyDict(knuts=25)

pileA['galleons'] += 10 # increase the number of galleons to 12
print(pileA) # prints {'galleons': 12, 'sickles': 10, 'knuts': 5}

It'd be nice to have some code that makes it easy to convert between the denominations or do basic math. We can't do something like pileA = pileA + pileB or pileA = pileA * 10 because the + and * operators don't work with dictionaries. Let's create a couple functions instead:

def makeWizardMoneyDict(galleons=0, sickles=0, knuts=0):
    return {'galleons': galleons, 'sickles': sickles, 'knuts': knuts}

def addWizardMoney(a, b):
    # adding two WizardMoney dictionaries
    return makeWizardMoneyDict(a['galleons'] + b['galleons'],
                               a['sickles'] + b['sickles'],
                               a['knuts'] + b['knuts'])

def multiplyWizardMoney(wizMon, n):
    return makeWizardMoneyDict(wizMon['galleons'] * n,
                               wizMon['sickles'] * n,
                               wizMon['knuts'] * n)

pileA = makeWizardMoneyDict(2, 10, 5)
pileB = makeWizardMoneyDict(knuts=25)

print(addWizardMoney(pileA, pileB))  # prints {'galleons': 2, 'sickles': 10, 'knuts': 30}
print(multiplyWizardMoney(pileA, 2)) # prints {'galleons': 4, 'sickles': 20, 'knuts': 10}

Notice that the addWizardMoney() and multiplyWizardMoney() functions return new dictionaries, rather than modify the dictionaries that were passed to them. This means these functions do not modify the dictionaries "in-place".

We could create more functions for subtraction, division, modulus, and other math operations, but let's move on for now.

Next, lets create functions that can take these dictionaries and convert their quantities to galleons, sickles, or knuts. We'll make the decision that our code won't deal with fractional amounts, so converting {'galleons': 0, 'sickles': 0, 'knuts': 35} to sickles will result in {'galleons': 0, 'sickles': 1, 'knuts': 6} and not {'galleons': 0, 'sickles': 1.20689655172413793, 'knuts': 0}.

Let's also add a valueOf() function that returns the number of knuts the dictionary is worth as an integer. Here's the code:

# Some constants for this currency:
KNUTS_PER_SICKLE= 29
SICKLES_PER_GALLEON = 17
KNUTS_PER_GALLEON = SICKLES_PER_GALLEON * KNUTS_PER_SICKLE

def convertToKnuts(wizMon):
    knuts = (wizMon['galleons'] * KNUTS_PER_GALLEON) + (wizMon['sickles'] * KNUTS_PER_SICKLE) + (wizMon['knuts'])
    return {'galleons': 0, 'sickles': 0, 'knuts': knuts}

def convertToSickles(wizMon):
    sickles = (wizMon['galleons'] * SICKLES_PER_GALLEON) + (wizMon['sickles']) + (wizMon['knuts'] // KNUTS_PER_SICKLE)
    knuts = (wizMon['knuts'] % KNUTS_PER_SICKLE) # These are knuts that weren't converted to sickles.
    return {'galleons': 0, 'sickles': sickles, 'knuts': knuts}

def convertToGalleons(wizMon):
    # First convert knuts to sickles so we have the max number of sickles to conver to galleons:
    sickles = (wizMon['sickles']) + (wizMon['knuts'] // KNUTS_PER_SICKLE)
    knuts = (wizMon['knuts'] % KNUTS_PER_SICKLE) # these are knuts that weren't converted to sickles

    # Then convert the sickles to galleons:
    galleons = (wizMon['galleons']) + (sickles // SICKLES_PER_GALLEON)
    sickles %= SICKLES_PER_GALLEON

    return {'galleons': galleons, 'sickles': sickles, 'knuts': knuts}

def valueOf(wizMon):
    return convertToKnuts(wizMon)['knuts']

amount = makeWizardMoneyDict(2, 50, 66)
print(amount) # prints {'galleons': 2, 'sickles': 50, 'knuts': 66}
print(convertToKnuts(amount))    # prints {'galleons': 0, 'sickles': 0, 'knuts': 2502}
print(convertToSickles(amount))  # prints {'galleons': 0, 'sickles': 86, 'knuts': 8}
print(convertToGalleons(amount)) # prints {'galleons': 5, 'sickles': 1, 'knuts': 8}

We can double-check the math of this to find that it is accurate.

With all of these functions packaged together in a file named winzmon.py, we now have a useful module for dealing with wizard money.

But this code is... less than ideal. It breaks if the dictionaries are missing keys, or the keys have typos. Doing math operations by calling functions is odd; using operators like in spam = 2 + 3 is much more readable than calling spam = add(2, 3).

Of course, for such a small bit of code, these functions are fine and OOP isn't needed. But let's scale up the complexity so that our code can do more.

What is Object-Oriented Programming?

OOP is a way of organizing code into classes. A class is a blueprint for making objects, including what data they contain and what methods they have. Objects are values that are created from the class.

Objects can be stored in a variable, passed to function call, or anything else a value can do. You can think of objects as values and classes as data types. The term instance is synonymous for object, and user-defined type is synonymous for class.

  • Class == User-Defined Type == Date Type == Type
  • Object == Instance == Value

Objects usually contain multiple values, just like how dictionaries are values that can contain multiple values. Objects can also have functions associated with them, just like string values have methods like lower() or startswith(). In this context, these functions are called methods.

There's many more features to OOP such as polymorphism, inheritance, and encapsulation, but let's ignore them for right now. (In my opinion, these features are overrated.) Lets just focus on making an object-oriented version of our previous wizard money code.

An OOP Example of Wizard Money

One of the chief strengths of Python is that you don't need to know much about computer science in order to write useful code. You don't need to know about object-oriented programming, but I hope the comparison between the non-OOP and OOP examples illustrates why it's a useful concept.

Let's create a class named WizardMoney. This class will be the blueprint for our WizardMoney objects. (These objects will fulfill the same role as the dictionaries in the previous non-oop example.) Open a file named wizmon.py and enter the following:

class WizardMoney:
    pass

The pass statement in Python literally does nothing. Python expects some indented code after a def statement, so we put pass there so that the Python code is syntactically correct until we come back around to finished it. This code will run, it just won't do anything.

We can play with it in the interactive shell though. By calling the class name as a function, we can create WizardMoney objects from the WizardMoney class. It's representation is a bit ugly though:

>>> from wizmon import *
>>> WizardMoney()
<__main__.WizardMoney object at 0x0000000002BAA860>

Beause of Python's dynamic nature, we can add attributes (called "member variables" in other languages) to these objects even though the class hasn't defined any. But normally we don't write code like this:

>>> x = WizardMoney()
>>> x.galleons = 5
>>> x.galleons
5

This makes you think that objects are sort of like dictionaries with key-value pairs, except you type x.galleons instead of x['galleons'], but classes offer much more. Instead of adding attributes to individual objects, we can define them in the initializer method __init()__ so that all WizardMoney objects have them:

class WizardMoney:
    def __init__(self):
        self.galleons = 0
        self.sickles = 0
        self.knuts = 0

There's a bit to unpack here. Creating an object from a class is done by calling the class name as a function: WizardMoney(). Creating the object calls the initializer method, which in Python is always called __init__(). The __init__() method automatically returns the newly created WizardMoney object, with its values initialized to 0. (There's no need to put a return statement in __init__().) Notice that the def statement must be indented so that it is inside the block following the class statement.

(If you are coming from another programming language, you may think that our term "initializer method" is the same thing as "constructor method" in those other languages. Technically, the constructor method in Python is __new__(), which does the actual object creation and runs __init__() afterwards to initialize any attributes. But 99% of the time you will be writing an __init__() method and not a __new__() method for your classes. There's a lot of more to __new__ and __init__ here.)

In code, Python methods always have self as a first parameter. This parameter contains a reference to the individual object on which the method was called. (Technically, it doesn't have to be named "self", but this is the convention in Python.) By having self.galleons = 0, we are saying that the object that the initializer was called on is having it's galleons attribute set to 0.

For example, let's add the conversion functions we wrote in the non-OOP example. We'll write them as methods in our WizardMoney class, so their def statements must be indented and inside the class method:

class WizardMoney:
    def __init__(self):
        self.galleons = 0
        self.sickles = 0
        self.knuts = 0

    def convertToKnuts(self):

        self._knuts += (self._galleons * KNUTS_PER_GALLEON) + (self._sickles * KNUTS_PER_SICKLE)
        self._galleons = 0
        self._sickles = 0


    def convertToSickles(self):

        self._sickles += (self._galleons * SICKLES_PER_GALLEON) + (self._knuts // KNUTS_PER_SICKLE)
        self._knuts %= KNUTS_PER_SICKLE # (some knuts may be remaining as change)
        self._galleons = 0


    def convertToGalleons(self):
        self._sickles += self._knuts // KNUTS_PER_SICKLE # convert knuts to sickles
        self._knuts %= KNUTS_PER_SICKLE # (some knuts may be remaining as change)

        self._galleons += self._sickles // SICKLES_PER_GALLEON # convert sickles to galleons
        self._sickles %= SICKLES_PER_GALLEON # (some sickles may be remaining as change)

Comments