14.3. Creating a Simple Class: WizCoin

Let’s create a WizCoin class, which represents a number of coins in a fictional wizard currency. In this currency, the denominations are knuts, sickles (worth 29 knuts), and galleons (worth 17 sickles or 493 knuts). Keep in mind that the objects in the WizCoin class represent a quantity of coins, not an amount of money. For example, it will inform you that you’re holding five quarters and one dime rather than $ 1.35 .

In a new file named wizcoin.py, enter the following code to create the WizCoin class. Note that the init method name has two underscores before and after init (we’ll discuss init in “Methods, init(), and self” later in this chapter):

>>> class WizCoin:
>>>
>>>     def __init__(self, galleons, sickles, knuts):
>>> # """Create a new WizCoin object with galleons, sickles, and knuts."""
>>>         self.galleons = galleons
>>>         self.sickles = sickles
>>>         self.knuts= knuts
>>> # NOTE: __init__() methods NEVER have a return statement.
>>>
>>>     def value(self):
>>> # """The value (in knuts) of all the coins in this WizCoin object."""
>>>         return (self.galleons * 17 * 29) + (self.sickles * 29) + (self.knuts)
>>>     def weightInGrams(self):
>>> # """Returns the weight of the coins in grams."""
>>>         return (self.galleons * 31.103) + (self.sickles * 11.34) + (self.knuts* 5.0)

This program defines a new class called WizCoin using a class state- ment 1. Creating a class creates a new type of object. Using a class statement to define a class is similar to def statements that define new functions. Inside the block of code following the class statement are the definitions for three methods: init() (short for initializer) 2, value() 3, and weightInGrams() 4. Note that all methods have a first parameter named self , which we’ll explore in the next section.

As a convention, module names (like wizcoin in our wizcoin.py file) are lowercase, whereas class names (like WizCoin ) begin with an uppercase let- ter. Unfortunately, some classes in the Python Standard Library, such as date , don’t follow this convention.

To practice creating new objects of the WizCoin class, enter the following source code in a separate file editor window and save the file as wcexample1.py in the same folder as wizcoin.py:

import wizcoin purse = wizcoin.WizCoin(2, 5, 99) # The ints are passed to __init__(). print(purse) print('G:', purse.galleons, 'S:', purse.sickles, 'K:', purse.knuts) print('Total value:', purse.value()) print('Weight:', purse.weightInGrams(), 'grams') print() coinJar = wizcoin.WizCoin(13, 0, 0) # The ints are passed to __init__(). print(coinJar) print('G:', coinJar.galleons, 'S:', coinJar.sickles, 'K:', coinJar.knuts) print('Total value:', coinJar.value()) print('Weight:', coinJar.weightInGrams(), 'grams')

The calls to WizCoin() 1 2 create a WizCoin object and run the code in the __init__() method for them. We pass in three integers as arguments to WizCoin() , which are forwarded to the parameters of __init__() . These argu- ments are assigned to the object’s self.galleons , self.sickles , and self.knuts attributes. Note that, just as the time.sleep() function requires you to first import the time module and put time. before the function name, we must also import wizcoin and put wizcoin. before the WizCoin() function name.

When you run this program, the output will look something like this:

<wizcoin.WizCoin object at 0x000002136F138080> G: 2 S: 5 K: 99 Total value: 1230 Weight: 613.906 grams

<wizcoin.WizCoin object at 0x000002136F138128> G: 13 S: 0 K: 0 Total value: 6409 Weight: 404.339 grams

If you get an error message, such as ModuleNotFoundError: No module named ‘wizcoin’ , check to make sure that your file is named wizcoin.py and that it’s in the same folder as wcexample1.py.

The WizCoin objects don’t have useful string representations, so print- ing purse and coinJar displays a memory address in between angle brackets. (You’ll learn how to change this in Chapter 17.)

Just as we can call the lower() string method on a string object, we can call the value() and weightInGrams() methods on the WizCoin objects we’ve assigned to the purse and coinJar variables. These methods calculate values based on the object’s galleons , sickles , and knuts attributes.

Classes and OOP can lead to more maintainable code—that is, code that is easier to read, modify, and extend in the future. Let’s explore this class’s methods and attributes in more detail. ## Methods, __init__(), and self Methods are functions associated with objects of a particular class. Recall that lower() is a string method, meaning that it’s called on string objects. You can call lower() on a string, as in ‘Hello’.lower() , but you can’t call it on a list, such as [‘dog’, ‘cat’].lower() . Also, notice that methods come after the object: the correct code is ‘Hello’.lower() , not lower(‘Hello’) . Unlike a method like lower() , a function like len() is not associated with a single data type; you can pass strings, lists, dictionaries, and many other types of objects to len() .

As you saw in the previous section, we create objects by calling the class name as a function. This function is referred to as a constructor function (or constructor, or abbreviated as ctor, pronounced “see-tore”) because it con- structs a new object. We also say the constructor instantiates a new instance of the class.

Calling the constructor causes Python to create the new object and then run the __init__() method. Classes aren’t required to have an __init() method, but they almost always do. The __init() method is where you commonly set the initial values of attributes. For example, recall that the __init__() method of WizCoin looks like the following:

>>> def __init__(self, galleons, sickles, knuts):
>>> # """Create a new WizCoin object with galleons, sickles, and knuts."""
>>>     self.galleons = galleons
>>>     self.sickles = sickles
>>>     self.knuts= knuts
>>> # NOTE: __init__() methods NEVER have a return statement.

When the wcexample1.py program calls WizCoin(2, 5, 99) , Python cre- ates a new WizCoin object and then passes three arguments ( 2 , 5 , and 99 ) to an __init__() call. But the __init() method has four parameters: self , galleons , sickles , and knuts . The reason is that all methods have a first parameter named self . When a method is called on an object, the object is automatically passed in for the self parameter. The rest of the arguments are assigned to parameters normally. If you see an error message, such as TypeError: __init() takes 3 positional arguments but 4 were given , you’ve probably forgotten to add the self parameter to the method’s def statement.

You don’t have to name a method’s first parameter self ; you can name it anything. But using self is conventional, and choosing a different name will make your code less readable to other Python programmers. When you’re reading code, the presence of self as the first parameter is the quickest way you can distinguish methods from functions. Similarly, if your method’s code never needs to use the self parameter, it’s a sign that your method should probably just be a function.

The 2 , 5 , and 99 arguments of WizCoin(2, 5, 99) aren’t automatically assigned to the new object’s attributes; we need the three assignment state- ments in __init__() to do this. Often, the __init() parameters are named the same as the attributes, but the presence of self in self.galleons indicates that it’s an attribute of the object, whereas galleons is a parameter. This stor- ing of the constructor’s arguments in the object’s attributes is a common task for a class’s __init() method. The datetime.date() call in the previous section did a similar task except the three arguments we passed were for the newly created date object’s year , month , and day attributes.

You’ve previously called the int() , str() , float() , and bool() functions to convert between data types, such as str(3.1415) returning the string value ‘3.1415’ based on the float value 3.1415 . Previously, we described these as functions, but int , str , float , and bool are actually classes, and the int() , str() , float() , and bool() functions are constructor functions that return new integer, string, float, and Boolean objects. Python’s style guide rec- ommends using capitalized camelcase for your class names (like WizCoin ), although many of Python’s built-in classes don’t follow this convention.

Note that calling the WizCoin() construction function returns the new WizCoin object, but the __init__() method never has a return statement with a return value. Adding a return value causes this error: TypeError: __init__() should return None . ## Attributes Attributes are variables associated with an object. The Python documenta- tion describes attributes as “any name following a dot.” For example, con- sider the birthday.year expression in the previous section. The year attribute is a name following a dot.

Every object has its own set of attributes. When the wcexample1.py pro- gram created two WizCoin objects and stored them in the purse and coinJar variables, their attributes had different values. You can access and set these attributes just like any variable. To practice setting attributes, open a new file editor window and enter the following code, saving it as wcexample2.py in the same folder as the wizcoin.py file:

import wizcoin change = wizcoin.WizCoin(9, 7, 20) print(change.sickles) # Prints 7. change.sickles += 10 print(change.sickles) # Prints 17. pile = wizcoin.WizCoin(2, 3, 31) print(pile.sickles) # Prints 3. pile.someNewAttribute = 'a new attr' # A new attribute is created. print(pile.someNewAttribute)

When you run this program, the output looks like this:

7 17 3 a new attr

You can think of an object’s attributes as similar to a dictionary’s keys. You can read and modify their associated values and assign an object new attributes. Technically, methods are considered attributes of a class, as well. ## Private Attributes and Private Methods In languages such as C++ or Java, attributes can be marked as having private access, which means the compiler or interpreter only lets code inside the class’s methods access or modify the attributes of objects of that class. But in Python, this enforcement doesn’t exist. All attributes and methods are effectively public access: code outside of the class can access and modify any attribute in any object of that class.

But private access is useful. For example, objects of a BankAccount class could have a balance attribute that only methods of the BankAccount class should have access to. For those reasons, Python’s convention is to start pri- vate attribute or method names with a single underscore. Technically, there is nothing to stop code outside the class from accessing private attributes and methods, but it’s a best practice to let only the class’s methods access them.

Open a new file editor window, enter the following code, and save it as privateExample.py. In it, objects of a BankAccount class have private _name and _balance attributes that only the deposit() and withdraw() methods should directly access:

>>> class BankAccount:
>>>     def __init__(self, accountHolder):
>>> # BankAccount methods can access self._balance, but code outside of
>>> # this class should not:
>>>
>>>         self._balance = 0
>>>
>>>         self._name = accountHolder
>>>         with open(self._name + 'Ledger.txt', 'w') as ledgerFile:
>>>             ledgerFile.write('Balance is 0\n')
>>>
>>>     def deposit(self, amount):
>>>         if amount <= 0:
>>>             return # Don't allow negative "deposits".
>>>         self._balance += amount
>>>         with open(self._name + 'Ledger.txt', 'a') as ledgerFile:
>>>             ledgerFile.write('Deposit ' + str(amount) + '\n')
>>>             ledgerFile.write('Balance is ' + str(self._balance) + '\n')
>>>     def withdraw(self, amount):
>>>         if self._balance < amount or amount < 0:
>>>             return # Not enough in account, or withdraw is negative.
>>>         self._balance -= amount
>>>         with open(self._name + 'Ledger.txt', 'a') as ledgerFile:
>>>             ledgerFile.write('Withdraw ' + str(amount) + '\n')
>>>             ledgerFile.write('Balance is ' + str(self._balance) + '\n')
>>> acct = BankAccount('Alice') # We create an account for Alice.
>>> acct.deposit(120) # _balance can be affected through deposit()
>>> acct.withdraw(40) # _balance can be affected through withdraw()
>>> # Changing _name or _balance outside of BankAccount is impolite, but allowed:
>>> acct._balance = 1000000000
>>> acct.withdraw(1000)
>>> acct._name = 'Bob' # Now we're modifying Bob's account ledger!
>>> acct.withdraw(1000) # This withdrawal is recorded in BobLedger.txt!

When you run privateExample.py, the ledger files it creates are inac- curate because we modified the _balance and _name outside the class, which resulted in invalid states. AliceLedger.txt inexplicably has a lot of money in it:

Balance is 0 Deposit 120 Balance is 120 Withdraw 40 Balance is 80 Withdraw 1000 Balance is 999999000

Now there’s a BobLedger.txt file with an inexplicable account balance, even though we never created a BankAccount object for Bob:

Withdraw 1000 Balance is 999998000

Well-designed classes will be mostly self-contained, providing methods to adjust the attributes to valid values. The _balance and _name attributes are marked as private 1 2, and the only valid way of adjusting the BankAccount class’s value is through the deposit() and withdraw() methods. These two methods have checks 3 5 to make sure _balance isn’t put into an invalid state (such as a negative integer value). These methods also record each transaction to account for the current balance 4 6.

Code outside the class that modifies these attributes, such as acct._­ balance = 1000000000 7 or acct._name = ‘Bob’ 8 instructions, can put the object into an invalid state and introduce bugs (and audits from the bank examiner). By following the underscore prefix convention for private access, you make debugging easier. The reason is that you know the cause of the bug will be in the code in the class instead of anywhere in the entire program.

Note that unlike Java and other languages, Python has no need for public getter and setter methods for private attributes. Instead Python uses properties, as explained in Chapter 17.