The Invent with Python Blog

Fri 17 August 2012

WizMon: An Object-Oriented Programming Tutorial for Python

Posted by Al Sweigart in misc   

Object-oriented programming (OOP) is a method of organizing your code that became popular in the 1990s with C++ and Java. It's a rather abstract concept, and explanations are usually vague or ambiguous, and filled with intimidating jargon like "polymorphism" and "encapsulation". Many tutorials use a Car class example, which in my opinion is a poor vehicle for explanation. (I never apologize for my puns.) A "car" represented by a Car class will be very different depending on if it's for a racing video game, a car dealership web app, or a traffic flow simulator.

These Car examples have simplified, meaningless code and I feel it contributes to OOP-for-the-sake-of-OOP. Object-oriented programming is a fantastic method to make large amounts of code manageable. At the same time, it's often overkill for small programs. Organizing your code into classes just because you feel like you should (which is what these Car class tutorials do) is busy work. 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 just red tape that gets in the way of real work.

In this tutorial, we will write Python code for a WizardMoney class 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). 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-using feature. Whatever code we write, the WizardMoney class will help us manage the currency-using 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.

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

A Non-OOP Example of Wizard Money

It'll be easier to manage these three denominations if we keep them together in a single value, so let's use dictionaries with keys 'galleons', 'sickles', and 'knuts'. This dictionary represents a certain amount of money:

myMoney = {'galleons': 2, 'sickles': 10, 'knuts': 5}
yourMoney = {'galleons': 0, 'sickles': 0, 'knuts': 25}

Also, let's make a function that generates these dictionaries 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}

myMoney = makeWizardMoneyDict(2, 10, 5)
yourMoney = makeWizardMoneyDict(knuts=25)

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 myMoney = myMoney + yourMoney or myMoney = myMoney * 10 because these are dictionaries and the plus and multiply 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)

myMoney = makeWizardMoneyDict(2, 10, 5)
yourMoney = makeWizardMoneyDict(knuts=25)

print(addWizardMoney(myMoney, yourMoney))

myMoney['galleons'] += 10
print(multiplyWizardMoney(myMoney, 2))

...which will print out:

{'galleons': 2, 'sickles': 10, 'knuts': 30}
{'galleons': 24, '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)
print(convertToKnuts(amount))
print(convertToSickles(amount))
print(convertToGalleons(amount))

...which will print out:

{'galleons': 2, 'sickles': 50, 'knuts': 66}
{'galleons': 0, 'sickles': 0, 'knuts': 2502}
{'galleons': 0, 'sickles': 86, 'knuts': 2}
{'galleons': 5, 'sickles': 1, 'knuts': 2}

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

With all of these functions packaged together in a .py file, we now have a useful module for dealing with wizard money. This code could be helpful if we're making a Harry Potter video game, or a currency calculator for a Harry Potter fan site, or any number of economics wizardry applications.

But this code is... less than ideal.

What is Object-Oriented Programming?

OOP is a way of organizing code into classes. A class is a blueprint for making objects. 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.

Objects usually contain multiple values, just like 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 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 much.

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.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()__ that gets called when we create a new object by calling WizardMoney():

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__().

(If you are coming from another programming language, you may think that our term "initializer" is the same thing as "constructor" in those other languages. Technically, the constructor method in Python is __new__(), which does the actual object creation and runs __init__() afterwards. But 99% of the time you will be writing an __init__() method and ignoring the __new__() method. 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.)

In our __init__() initializer method, the newly created WizardMoney object has attributes galleons, sickles, and knuts all set to 0. You could

Comments