16. Classes, Constructors, and Attributes

Video link ../../_images/construction.svg

Throughout this course we’ve been using variables to store a value. We just learned how to store multiple values using a list. The next step is object-oriented programming. This type of programming has three advantages. One, we can group multiple variables together in a single record. Two, we can associate functions with that group of data. Three, we can use something called inheritance which allows us to take a base set of code and extend it, without needing to rewrite it from scratch.

16.1. Using Classes and Objects to Group Data

../../_images/character.svg

Grouping related data together using object-oriented programming can simplify our code. For example, think of an adventure game. Each character in an adventure game needs data, such as a name, what they look like, how many hit points they have, their armor, and how fast they can move.

Without classes, our Python code to store the information might look like this:

1
2
3
4
5
6
name = "Link"
outfit = "Green"
max_hit_points = 50
current_hit_points = 50
armor_amount = 6
max_speed = 10

In order to do anything with this character, we’ll need to pass all that data to a function. With so many parameters, that function gets complex and hard to manage.

1
2
def display_character(name, outfit, max_hit_points, current_hit_points, armor, max_speed):
    print(name, outfit, max_hit_points, current_hit_points)

As our game expands, we might start adding more character attributes, such as weapons, magic, special abilities, and more. To do that we’d have to go through each function in our program that works with the player character and redo the parameters.

Keeping all these data points organized becomes difficult very quickly. How do we keep a monster’s hit points separated from the player’s hit points? Because when we add monsters to the game, they’ll have their own attributes. In fact, just about every item in an adventure game has attributes.

There needs to be a better way. Somehow our program needs to package up all those data fields for easy management.

16.2. Defining Classes

A better way to manage multiple data attributes is to define a structure to hold the information. We can give that “grouping” of information a name, like Character or Address. This can be easily done in Python and any other modern language by using a class. Each data item we group into the class is called a field, attribute, or instance variable. These terms may be used interchangeably, as they mean the same thing.

16.2.1. Defining the Class

Let’s code an example using our adventure character. First, we tell the computer we are defining a class with the class keyword, and then we give the class a name that starts with a capital letter. Just like with functions and loops, we end the statement with a colon, and everything associated with the class will be indented below it:

1
class Character:

Unlike variables, all class names should start with a capital letter. While you can use a lower-case variable, you never should. Following this pattern of lower-case for variables and upper-case for classes makes it easy to tell which is which.

Next, we normally put into triple-quote comments a description of the class.

1
2
3
4
class Character:
    """
    This is a class that represents the player character.
    """

Yes, the code will run fine without any comments. It is optional. However good documentation is important to maintainable code, even if you are the only person using the code.

The cool feature about creating comments this way, is the text can be pulled out automatically to form a website for your API documentation. All the classes and functions in the Arcade library’s API are created with these comments. You can see the result here:

https://api.arcade.academy/en/latest/arcade.html

For each of those examples, you can click on the “source” link and quickly go to the source code for that function or class.

16.2.2. Defining the Init Function

Any time we create a new instance of a class, we need code that will create our attributes (variables) and set them to default values. In Python, this is the __init__ method.

This strangely named method needs a bit of explanation.

First, any function in a class is called a method, rather than a function. This helps us keep straight what is in a class, and what isn’t.

Second, the initialization method is a magic method that is called automatically. Yes, Python programmers actually call methods that are automatically invoked “magic methods.”

Third, to signify a method is magic, Python surrounds the method name with double underscores. Two underscores in the front, and two underscores in the back. The short-name for double-underline is dunder, and these magic methods are also known as dunder methods.

1
2
3
4
5
6
class Character:
    """
    This is a class that represents the player character.
    """
    def __init__(self):
        """ This is a method that sets up the variables in the object. """

The most common mistakes people make when typing this in is to use only one underscore before and after the init, and to forget that there is a space between def and the first underscore.

All methods in a class have at least one parameter, and the first parameter is always self. We’ll explain about self in the next section.

16.2.3. Defining Class Attributes

../../_images/healthbar.png

Remember back to our chapter on functions, that any variable created inside a function is forgotten about after the function is done running? If you want to keep anything, you need to return it as a value.

Methods follow this rule too, with one exception. The self parameter refers to memory associated with each instance of the class. We can use that self to create variables that keep their value for as long as the object exists. We call variables that exist as part of the class either attributes, fields, or instance variables. The terms mean the same thing. Attributes must be set to a default value. That value is often 0, an empty string, or the special value None.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Character:
    """
    This is a class that represents the player character.
    """
    def __init__(self):
        """ This is a method that sets up the variables in the object. """
        self.name = ""
        self.outfit = ""
        self.max_hit_points = 0
        self.current_hit_points = 0
        self.armor_amount = 0
        self.max_speed = 0

In the example above, if we had failed to put self. in front, the computer would completely forget about the variables once the __init__ function was done.

Here’s another example, we are defining a class called Address which has attributes for each field of a US mailing address.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Address:
    """ Hold all the fields for a mailing address. """
    def __init__(self):
        """ Set up the address fields. """
        self.name = ""
        self.line1 = ""
        self.line2 = ""
        self.city = ""
        self.state = ""
        self.zip = ""

In the code above, Address is the class name. The variables in the class are the attributes.

The __init__ is a special method that you may also hear referred to as a constructor. If you are programming in other languages, the term constructor is a generic term used to refer to whatever that language’s equivalent to the __init__ method is.

The self. is kind of like the pronoun my. When inside the class Address we are talking about my name, my city, etc. We don’t want to use self. outside the class. Why? Because just like the pronoun “my,” it means someone totally different when said by a different person!

16.3. Creating Objects

../../_images/address.svg

The class code defines a class but it does not actually create an instance of one. The code told the computer what fields an address has, but we don’t actually have an address yet. We can define a class without creating one just like we can define a function without calling it.

To create an instance of the Address class, we use the following code:

1
2
3
def main():
    # Create an address
    home_address = Address()

We need a variable that will point to our address. In this case, we’ve called it home_address. We’ll set that variable equal to the new instance of the class we create. We create an new instance by using the name of the class (Address), followed by parentheses. This will “magically” call the __init__ method which will set up fields/attributes for the class.

In this case, Address is a class. It defines what an address looks like. The home_address variable points to an object. An object is an instance of a class. It is the actual address. As another example, “Human” is a class, while “Samantha” and “Pete” are instances of the class.

You can set the object’s attributes using the dot operator. First, use the variable that points to our object, immediately follow that with a period, then the attribute name.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def main():
    # Create an address
    home_address = Address()

    # Set the fields in the address
    home_address.name = "John Smith"
    home_address.line1 = "701 N. C Street"
    home_address.line2 = "Carver Science Building"
    home_address.city = "Indianola"
    home_address.state = "IA"
    home_address.zip = "50125"

A second variable can be created that points to a completely different instance of the Address class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    # Create another address
    vacation_home_address = Address()

    # Set the fields in the address
    vacation_home_address.name = "John Smith"
    vacation_home_address.line1 = "1122 Main Street"
    vacation_home_address.line2 = ""
    vacation_home_address.city = "Panama City Beach"
    vacation_home_address.state = "FL"
    vacation_home_address.zip = "32407"

    print("The client's main home is in " + home_address.city)
    print("His vacation home is in " + vacation_home_address.city)

Attributes are not limited to being simple strings and numbers! If you have a class that represents a graph, you can store all the data points in an attribute that is a list. Attributes can even be other objects. An object that represents a player character in an adventure could have an attribute with another object that represents a magical hat.

16.3.1. Common Mistakes Creating Objects

The first common mistake when creating an object is to forget the parentheses:

1
2
# ERROR - Forgot the parentheses after Address
home_address = Address

The terrible thing about this mistake is that the program won’t stop or give you an error. Try running the example we just created with the two different addresses. Take out the parentheses. The program runs without error, but both the vacation home and the home address say we are in Panama City! That’s because without the parentheses we don’t create a new address, we just use the same block of memory and write the new information over the old, so everything points to the same address.

Another very common mistake when working with classes is to forget to specify which instance of the class you want to work with. If only one address is created, it is natural to assume the computer will know to use that address you are talking about. This is not the case.

Take a look at this code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Address:
    def __init__(self):
        self.name = ""
        self.line1 = ""
        self.line2 = ""
        self.city = ""
        self.state = ""
        self.zip = ""

def main():
    # Create an address
    my_address = Address()

    # Alert! This does not set the address's name!
    name = "Dr. Smith"

    # This doesn't set the name for the address either
    Address.name = "Dr. Smith"

    # This runs, creates a new attribute but with the wrong name.
    my_address.naem = "Dr. Smith"

    # This does work:
    my_address.name = "Dr. Smith"

main()

This code will run without generating an exception, but it still isn’t correct. Line 15 creates a variable called name, but it is completely different than the name that is part of Address. So we think we’ve set the name, but we haven’t.

Line 18 does refer to Address, but not my_address. Frustratingly it runs without alerting us to an error, but the code isn’t modifying my_address. Instead it sets something called a static variable, which we’ll talk about later.

Think of it this way. If you are in a room of people, saying “Age is 18” is confusing. Saying “Human’s age is 18” is also confusing. Saying “Sally’s age is 18” is ideal, because you are saying which instance of human you are referring to. You have to do this with programming, even if there is only one human in the room.

Another mistake is on line 22. That line also runs fine, but it creates a new attribute called naem instead of setting the desired attribute name.

16.4. Using Objects in Functions

Putting lots of data fields into a class makes it easy to pass data in and out of a function. In this example, the function takes in an address as a parameter and prints it out on the screen. It is not necessary to pass parameters for each field of the address.

Passing in an object as a function parameter
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def print_address(address):
    """ Print an address to the screen """

    print(address.name)
    # If there is a line1 in the address, print it
    if len(address.line1) > 0:
        print(address.line1)
    # If there is a line2 in the address, print it
    if len(address.line2) > 0:
        print( address.line2 )
    print(address.city + ", " + address.state + " " + address.zip)


def main():
    # ... code for creating home_address and vacation_home_address
    # goes here.
    print_address(home_address)
    print()
    print_address(vacation_home_address)


main()

16.5. Customizing the Constructor

../../_images/dog.svg

Take a look at this code, where we represent a dog using a class. Unfortunately, there’s a terrible problem with the code. When we create a dog, the dog has no name. Dogs should have names! Only horses in the desert can have no name.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Dog():
    def __init__(self):
        """ Constructor """
        self.name = ""


def main():
    # This creates the dog
    my_dog = Dog()
    print(f"The dog's name is: {my_dog.name}")


main()

We can modify the code in our constructor to keep this from happening. First, let’s add a print statement to our __init__ just to demonstrate that it is really being called.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Dog():
    def __init__(self):
        """ Constructor """
        self.name = ""
        print("A new dog is born!")


def main():
    # This creates the dog
    my_dog = Dog()
    print(f"The dog's name is: {my_dog.name}")

When the program is run, it will print this:

A new dog is born!

When a Dog object is created on line 10, the __init__ function is “magically” called and the message is printed to the screen.

We can add a parameter to our constructor, so that it requires us to pass in a name for the dog. Try running this code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Dog():
    def __init__(self, new_name):
        """ Constructor """
        self.name = new_name
        print("A new dog is born!")


def main():
    # This creates the dog
    my_dog = Dog()
    print(f"The dog's name is: {my_dog.name}")


main()

You should get an error that looks like:

File "c:/my_project/test.py", line 10, in main
  my_dog = Dog()
TypeError: __init__() missing 1 required positional argument: 'new_name'

The computer is saying it is missing a value for the new_name parameter. It won’t let the dog be created without a name. We can fix that up by adding a name when we create the dog.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Dog():
    def __init__(self, new_name):
        """ Constructor """
        self.name = new_name
        print("A new dog is born!")


def main():
    # This creates the dog
    my_dog = Dog("Fluffy")

Notice in line 4 we take the value that was passed in as a parameter and assign self.name to have that same value. Without this line, the dog’s name won’t get set.

As programmers sometimes get tired of making up variable names, it is completely normal to see code like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Dog():
    def __init__(self, name):
        """ Constructor """
        self.name = name
        print("A new dog is born!")


def main():
    # This creates the dog
    my_dog = Dog("Fluffy")


main()

Though it may seem strange at first, we have two variables at work, not one. The first variable is name, and that variable is assigned as a parameter when we call the Dog constructor. It goes away as soon as the Dog constructor is done, and is forgotten about. The second variable is self.name, and that variable is complete different than name. Its value will stay after the constructor is done.

16.6. Address Class With Init Parameters

Here’s another example, this time with our Address class. We supply the address attributes as part of our __init__ when our address is created.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Address:
    def __init__(self, line1, line2, city, state, zip, country):
        self.line1 = line1
        self.line2 = line2
        self.city = city
        self.state = state
        self.zip = zip
        self.country = country


def main():
    # This creates the address
    my_address = Address("701 N. C Street",
                         "Carver Science Building",
                         "Indianola",
                         "IA",
                         "50125",
                          "USA")


main()

16.7. Typing Attributes

It is possible to tell Python what type of data should be stored in a class attribute. This allows a programmer to use a tool like mypy and catch errors earlier in the development process.

In this example, we are adding a type definition to the name attribute on line 3. We do this by following the variable name with a colon, and adding str which is the abbreviation for the string data type.

1
2
3
4
5
6
7
class Person:
    def __init__(self):
        self.name: str = "A"


mary = Person()
mary.name = 22

By assigning a number to the name attribute on line 7, we are storing the wrong kind of data. The program runs, but if we use the mypy tool, it will give us an error saying we’ve made a mistake:

1
2
test.py:7: error: Incompatible types in assignment (expression has type "int", variable has type "str")
Found 1 error in 1 file (checked 1 source file)

Typing is great for large programs, and for programs where we want to make sure to catch all the errors we can before shipping to customers.

As we are just learning programming, it can be distracting to try adding typing to our programs at this stage. But we will be both looking and using, other people’s code which does use typing. Therefore it is important to know what typing is, even if we don’t need to use it ourselves until later.

16.8. Data Classes

When creating a class and a constructor to define a set of fields, we end up with code that looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Address:
    def __init__(self,
                 name: str = "",
                 line1: str = "",
                 line2: str = "",
                 city: str = "",
                 state: str = "",
                 zip_code: str = ""
                 ):
        self.name: str = name
        self.line1: str = line1
        self.line2: str = line2
        self.city: str = city
        self.state: str = state
        self.zip_code: str = zip_code

This code is repetitive, as we state the fields twice. If your __init__ method is only going to take in data fields and assign attribute values, you can simplify your code by using a dataclass.

Starting with Python 3.8, you can write the same thing using only this code:

1
2
3
4
5
6
7
8
@dataclass
class Address:
    name: str = ""
    line1: str = ""
    line2: str = ""
    city: str = ""
    state: str = ""
    zip_code: str = ""

This makes the code a lot easier to both write, and to read.

16.9. Static Variables

../../_images/cat.svg

Class attributes are also called instance variables because they can be different for each instance of the class. If you have five instances of the Dog class, each instance will have its own name.

In a few rare cases, we want to share data between all instances of a class. In this example with a Cat class, we have a population variable. This variable is not different for each cat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Cat:
    population = 0

    def __init__(self, name):
        self.name = name
        Cat.population += 1

def main():
    cat1 = Cat("Pat")
    cat2 = Cat("Pepper")
    cat3 = Cat("Pouncy")

    print("The cat population is:", Cat.population)

main()

In this case we use Cat.population to keep track of our cat population, and the program will print out the correct count of 3.

Variables that don’t change for each instance of a class, are called class variables or static variables. The terms mean the same thing and can be used interchangeably.

You refer to a static variable by using the class name Cat rather than any of the instance names like cat1.

Static variables aren’t used that often. The only reason we are introducing them here is that it is not unusual for students to accidentally use a static variable instead of an instance variable. In fact, Python makes it a bit too easy to ‘blend’ the two concepts together.

For example, we can also print a static variable not just by using the class name, but also by using the instance name:

1
2
print("The cat population is:", Cat.population)
print("The cat population is:", cat1.population)

When we are reading code and come across a variable like Cat.population, we immediately know it is static. How? All class names start with a capital letter, so Cat is a class. The only attributes that we can refer to with a class, rather than an instance, are static variables. So population must be static. If we use cat1.population, a programmer reading that code might mistakenly assume it is an instance variable rather than a static variable, so that makes debugging really hard. To reduce confusion, always refer to static variables using the class name.

In this example, I set population to 4, and each print statement says population is 4. This is confusing because I set one variable and the others change. If I just use Cat.population to refer to the population, then I remove that confusion.

1
2
3
4
Cat.population = 4
print("The cat population is:", Cat.population)
print("The cat population is:", cat2.population)
print("The cat population is:", cat1.population)

Here’s where it gets really wild. As we just saw, I can print a static variable by referring to it with an instance, rather than by the class name. I shouldn’t, but I can.

What if, instead of printing, I assign a value that way?

1
2
3
4
5
6
Cat.population = 4
cat3.population = 5
print("The cat population is:", Cat.population)
print("The cat population is:", cat1.population)
print("The cat population is:", cat2.population)
print("The cat population is:", cat3.population)

In this case Cat.population, cat1.population, and cat2.population all refer to the same static variable. But once I assign a value to cat3.population it creates a brand-new instance variable. So all the other cats use the static population value, while cat3 uses a new instance variable with the same exact name as the static variable. The static variable is shadowed by the instance variable. Therefore when we print cat3.population we get a 5. That type of bug is very hard to find.

For our purposes, we won’t need to use static variables, we only introduce them so that you can better understand some confusing errors people occasionally run into.

16.10. Review

In this chapter we learned how to bundle together several related data items into a class. We call these class attributes, instance variables, or fields. Each instance of a class is an object. Functions defined in a class are called methods. A special magic method called when an object is created is the __init__ method, which is used to set up instance variables and assign them their initial values.

Inside the class we refer to instance variables by putting self. in front of them, such as self.name. Outside the class, we need to use a variable that refers to the class, such as customer.name.

Using classes helps simplify our code. We can use classes to represent:

  • Characters in a video game, with attributes for health, speed, and armor.

  • Graphs, with attributes for heading, size, and data.

  • A customer order, with a list as an attribute for each item in the order.

Data classes can be used to make it easier to define a class with a lot of attributes. Typing can be used to make sure we don’t put the wrong type of data in an attribute. Static variables are attributes that don’t change from object to object.

16.10.1. Review Questions

  1. What are the three main advantages of object-oriented programming?

  2. What keyword is used to define a new class?

  3. All class names should start with an upper-case or lower-case letter?

  4. Where do the comments for a class go? What kind of comments do you use? Why is there a standard?

  5. What is the difference between a function and a method?

  6. What three different terms can be used to refer to data that is tied to a a class?

  7. What is a magic method?

  8. What is a dunder method?

  9. All class methods should have start with the same parameter. What is that parameter?

  10. What is the name of the method in a class where we define our attributes?

  11. When defining a class attribute, what needs to go right before it?

  12. What is a constructor?

  13. What is the difference between a class and an object?

  14. What are the common mistakes when creating instances (objects) of a class?

  15. How can we make sure our attributes are assigned when the object is created?

  16. What is the point of adding “typing” to a class?

  17. What is a data class?

  18. What are static variables?

16.10.2. Lab 6: Text Adventure

In Lab 6: Text Adventure, you’ll use a class to represent a room in an text adventure. You’ll use attributes to store the room description, and which rooms are north, south, east and west of it. You’ll use a list to store all the rooms in your adventure.