Chapter 5.P

Python Classes

Classes in Python

Subsections of Python Classes

Classes

Creating a Class

Creating a class in Python is very similar to creating a function. We can simply use the class keyword, followed by an identifier for the class.

While it is not required, it is recommended to place just a single class in each file, with the filename matching the name of the class, followed by the .py file extension. By convention, class names in Python are written in CamelCase, meaning that each word is capitalized and there are no spaces between the words. We’ll follow these rules in our code.

So, to create an empty class named Student, we would place the following code in a file named Student.py:

class Student:
  pass

As we’ve already learned, each class declaration in Python includes these parts:

  1. class - this keyword says that we are declaring a new class.
  2. Student - this is an identifier that gives us the name of the class we are declaring.

Following the declaration, we see a colon : marking the start of a new block, inside of which will be all of the fields and methods stored in this class. We’ll need to indent all items inside of this class, just like we do with other blocks in Python.

In order for Python to allow this code to run, we cannot have an empty block inside of a class declaration. So, we can add the keyword pass to the block inside of the class so that it is not empty. pass tells the compiler to do nothing. It is also referred to as a “no-op”, which has its roots in assembly, a very low level language. High level languages typically use a no-op when their syntax rules require that a code-block exists, but the program requires that code block do nothing.

To the left, we should see three Python files open: Student.py, Teacher.py, and Main.py. Let’s go ahead and add the class declaration code to each file. Once we’ve completed that, we can use the button below to check our code and confirm that it is working before moving ahead.

Class Attributes

Types of Attributes

There are two types of attributes: Class and Instance.

Class attributes are variables shared by all the instances (unique objects) in a class. They are typically things like constants, or information about how many objects of the class exists. For example a Stop_Light class may have the class attribute light _color = ['red', 'yellow', 'green'] to ensure all instances of Stop_Light use the same colors in the same order. A more complex version of our Student class might have a list of all in-use student_ids to ensure they are all unique.

Class attributes are denoted on the UML diagram by underlining the attribute. In our example the Main-class contains Class Attributes.

Many programming languages include a special keyword static. In those languages, a static attribute or method is what Python calls a class attribute or method. CLass items are indicated on UML diagram by an underline. For Python, static-method and static-attribute are synonymous with class-.1

Class Attributes

In Python, any attributes declared outside of a method are class attributes, but they can be considered the same as static attributes until they are overwritten by an instance. Here’s an example:

class Stat:
  x = 5     # class or static attribute
  
  def __init__(self, an_y):
    self.y = an_y # instance attribute

In this class, we’ve created a class attribute named x, and a normal attribute named y. Here’s a main() method that will help us explore how the static keyword operates:

from Stat import *

class Main:
  
  def main():
    some_stat = Stat(7)
    another_stat = Stat(8)
    
    print(some_stat.x)     # 5
    print(some_stat.y)     # 7 
    print(another_stat.x)  # 5
    print(another_stat.y)  # 8
    
    Stat.x = 25           # change class attribute for all instances
    
    print(some_stat.x)     # 25
    print(some_stat.y)     # 7 
    print(another_stat.x)  # 25 
    print(another_stat.y)  # 8
    
    some_stat.x = 10      # overwrites class attribute in instance
    
    print(some_stat.x)     # 10 (now an instance attribute)
    print(some_stat.y)     # 7 
    print(another_stat.x)  # 25 (still class attribute)
    print(another_stat.y)  # 8

if __name__ == "__main__":
    Main.main()

First, we can see that the attribute x is set to 5 as its default value, so both objects some_stat and another_stat contain that same value. Interestingly, since the attribute x is static, we can access it directly from the class Stat, without even having to instantiate an object. So, we can update the value in that way to 25, and it will take effect in any objects instantiated from Stat.

Below that, we can update the value of x attached to some_stat to 10, and we’ll see that it now creates an instance attribute for that object that contains 10, shadowing the previous class attribute. The value attached to another_stat is unchanged.

You should avoid shadowing class attributes.

Class Methods

Python also allows us to create class methods that work in a similar way:

class Stat:
  x = 5     # class or static attribute
  
  def __init__(self, an_y):
    self.y = an_y # instance attribute
  
  def sum(a):
    return Stat.x + a

We have now added a class method sum() to our Stat class.

In addition, it is important to remember that a static method cannot access any non-static attributes or methods, since it doesn’t have access to an instantiated object in the self parameter.

As a tradeoff, we can call class method without instantiating the class either, as in this example:

from Stat import *

class Main:
  
  def main():
    # other code omitted
    Stat.x = 25
    
    moreStat = Stat(7)
    print(moreStat.sum(5))  # 30
    print(Stat.sum(5))      # 30

if __name__ == "__main__":
    Main.main()

Alternative Syntax

Python supports an alternate syntax for methods. This form has not been widely adopted.

class Demo:
     class_count = 0      # a class variable 
        
    def __init__():
        self.name = None

    def instance(self, name):    # an instance method (note the self parameter) 
                                 # call <variable>.instance("text")
        self.name = name         # instance attribute
        Demo.class_count += 1    # access a class attribute via class name
        
    @classmethod
    def print_count(cls):        # class method
                                 # call Demo.print_count()
        print (cls.class_count)  # access class variable via 'cls'
        
    @staticmethod 
    def total_number():          # static method
                                 # call Demo.total_number
        return Demo.class_count  # access a class attribute via class name

There is no expressive power difference between an @classmethod and @staticmethod–the can access exactly the same things using slightly different syntax. The @staticmethod is called a decorator. The decorators tell the Python interpreter to handle the item that follows in a special way.

By convention, the object oriented programs start with a static main methods. We will follow that convention.

UML Class Diagrams

UML neatly side steps the static-vs-class nomenclature debate by using the term “classifer” for any item which belongs to the class and not the instance. According to the UML specification, any “classifer” items should be underlined, as in this sample UML diagram below:

UML Class Diagram with Static Items UML Class Diagram with Static Items

Try It!

Type (or cut and paste) the code for the Stats class (in static/Stat.py) and Main (in static Main.py). Mark the main() method with an @staticmethod decorator. Now, we can use the buttons below to run the sample files open to the left to explore using class attributes and methods.


  1. There are technical differences in the way languages store and access static and class items at the machine code level. We will ignore these differences. ↩︎

Subsections of Class Attributes

Access Control

YouTube Video

Video Materials

Note: the practice of making class identifiers starting with `__` hard to call is called mangling. "Mangling" is a Python term, not a generic coding term, and was originally introduced to prevent name-collisions (shadowing) in child classes.

The reason one can call `__init__()` is that the pattern `__name__` is used for Python language support elements. The trailing `__`, overrides any mangling.

As we work on developing our classes, we may want to protect data and methods stored in those classes. While Python doesn’t include any security modifiers that enforce these restrictions, there are some conventions used in Python code to denote attributes and methods that should not be accessed outside the class. Let’s review those and see how they work in our programs.

Private Attributes and Methods

In Python, we can use two underscores __ in front of the name of any attribute or method in the class to denote that it should be private. This will let other developers know that those attributes and methods should not be accessed from outside the class.

Let’s look at an example to see how this would work:

class Security:
  
  def __init__(self):
    self.__reset()
    
  def count(self):
    return len(self.name)
  
  def __reset(self):
    self.name = "test"
    self.__secret = 123

In this class, we have created both a private and a public attribute, and a private and a public method. We can also see that we are able to call the private method __reset() from within our constructor, and in the reset() method we are able to access the secret private attribute without an issue. So, within our class itself, we don’t have to worry about not being able to access anything we need.

However, outside of that class, they have a big impact on what we can easily access. Consider the following main() method from a different class:

from Security import *

class Main:
  
  def main():
    some_security = Security()
    print(some_security.name)      # "test"
    print(some_security.__secret)  # AttributeError
    print(some_security.count())   # 4
    some_security.__reset()        # AttributeError
    
if __name__ == "__main__":
    Main.main()

In this code, we cannot access any private members of the Security class. So, when we try to run this code, we’ll get the following error message:

Traceback (most recent call last):
  File "11p-classes/security/Main.py", line 13, in <module>
    Main.main()
  File "11p-classes/security/Main.py", line 8, in main
    print(some_security.__secret)  # AttributeError
AttributeError: 'Security' object has no attribute '_Main__secret'

As we can see, Python itself has a method for enforcing these security modifiers, making a very powerful way to limit access to the members and attributes in our classes.

Unfortunately, Python does have a way to get around these restrictions as well. Instead of referencing __secret, we can instead reference _Security__secret to find that value, as in this example:

from Security import *

class Main:
  
  def main():
    some_security = Security()
    print(some_security.name)               # "test"
    print(some_security._Security__secret)  # 123
    print(some_security.count())            # 4
    some_security._Security__reset()        # No errors
    
if __name__ == "__main__":
    Main.main()

Behind the scenes, Python adds an underscore _ followed by the name of the class to the beginning of any class attribute or method that is prefixed with two underscores __. So, knowing that, we can still access those attributes and methods if we want to. Thankfully, it’d be hard to do this accidentally, so it provides some small level of security for our data.

Why Limit Access?

So, why would we want to do this? After all, if we’re the ones writing the code, shouldn’t we be able to access everything anyway?

In many cases, the classes we are writing become part of a larger project, so we may not always be the ones writing code that interfaces with our class. For example, if we are writing an engine for a video game, we may want to make attributes such as the player’s health private. In that way, anyone writing a mod for the engine would not be able to easily modify those values and cheat the system. Similarly, there are some actions, such as a reset() method, that we may only want to call from within the class.

As we build larger and more complex programs, we’ll even find that it is helpful as developers to limit access to just the methods and attributes that should be accessed by anyone using our classes, helping to simplify our overall program’s structure.

Limiting access is part of encapsulation, one of the pillars of object oriented programming.

UML Class Diagrams

We can denote the access level of items in a UML class diagram using a few simple symbols. According to the UML standard, any item with a plus + in front of it should be public, and any item with a minus - in front should be private. So, we can build a UML class diagram showing the Main and Security classes as shown below:

UML Class Diagram with Security UML Class Diagram with Security

Subsections of Access Control

Attributes & Initialization

Of course, our classes are not very useful at this point because they don’t include any attributes or methods. Including attributes in a class is one of the basic uses of classes, so let’s start there.

Types of Attributes

There are two types of attributes: Class and Instance.

Class attributes are variables shared by all the instances (unique objects) in a class. They are typically things like constants, or information about how many objects of the class exists. For example a Stop_Light class may have the class attribute light _color = ['red', 'yellow', 'green'] to ensure all instances of Stop_Light use the same colors in the same order. A more complex version of our Student class might have a list of all in-use student_ids to ensure they are all unique.

Class attributes are denoted on the UML diagram by underlining the attribute. In our example the Main-class contains Class Attributes.

Instance attributes are variables that “belong” to the instance. It makes sense that a Student-object owns its own name.

You can see a better discussion of how each type of variable works at these links:

UML Class Diagram showing Main, Student, and Teacher Classes, Attributes, and Methods UML Class Diagram showing Main, Student, and Teacher Classes, Attributes, and Methods

Class Attributes

Class attributes are added directly the class body. In this case we use empty lists as the default values.

class Main:
    students = []
    teachers = []

Class attributes are accessed by using the class_name.attribute (Main.students).

Instance Attributes and the Initializer

To add an instance attribute to a class, we can simply place a variable assignment inside of a special method called the initializer.

class Student:
    
    def __init__(self):
        self.name = "name"
        self.age = 19
        self.student_id = "123456987"
        self.credits = 0
        self.gpa = 0.0

There are a couple of things to note. First the use of __ to begin and end the method name. Python uses this convention to identify methods, variable names, etc for which the Python interpreter has special uses.

There is a default __init__() will run for all objects; its body is basically pass. However, if the class definition overrides it, by including its own definition for __init__(), the class’s definition is used. Overriding a method is a type of polymorphism.

Next is the keyword self. Python uses self to refer to the specific object accessing the code. It is the mechanism that ensures the Student-object “Bob” sees Bob’s data and the Student-object “Mari” sees Mari’s data. Whenever you see a method where the first parameter is self, it is an instance method.

To access an instance attribute, use the full name, self.varible_name.

self is a Python specific implementation. As a result, self will never be included in the UML parameter list for methods or used in the UML attribute names. When the UML calls for instance methods/attributes insert self into the code.

Try Not to Obscure

In Python the following is acceptable syntax.

class Example:
    name = "class"  # a class attribute
    
    def __init__(self):
        self.name = "instance"
        
    def method(self):
        name = "local"
        ...

It is unacceptable style. In method, there are now three variables called ’name':

  • name, with a value if “local”;
  • self.name, with a value if “instance”;
  • Example.name, with a value of “class”.

Code written in this style is difficult to read and understand, as such, avoid it.

Note that it is common in __init__ to see

def __init__ (self, name):
    self.name = name

Later in this chapter we’ll discuss ways that we can indicate to other developers that these attributes should or should not be accessed outside of this class.

For now, let’s go ahead and add the correct attributes and __init__ methods in Student.py, Teacher.py and the class attributes in Main.py files. Feel free to refer to the UML diagram below to find the correct attributes for each class. For the items in Main.py, we’ll just create an empty list using [] for now.

Subsections of Attributes & Initialization

Methods

We can also add additional methods to our classes. These methods are used either to modify the attributes of the class or to perform actions based on the attributes stored in the class. Finally, we can even use those methods to perform actions on data provided as arguments. In essence, the sky is the limit with methods in classes, so we’ll be able to do just about anything we need to do in these methods.

As with attributes, there are class and instance methods. Class methods have access to their parameters and class attributes, which the access via the syntax class_name.attribute_name. Instance methods by convention have their first parameter as self and have access to their parameters, class attributes and their instance attributes (self.attribute_name). Class methods are underlined on the UML diagram.

Adding Instance Methods

To add a method to our class, we can simply add a method declaration inside of our class declaration. So, let’s add the methods we need to our Student class:

class Student:
  def __init__(self):
    self.name = "name"
    self.age = 19
    self.student_id = "123456987"
    self.credits = 0
    self.gpa = 0.0
    
  def birthday(self):
    self.age += 1
   
  def grade(self, credits, grade_points):
    current_points = round(self.gpa * self.credits)
    self.credits += credits
    current_points += grade_points
    self.gpa = current_points / self.credits

The birthday() method is pretty straightforward. When that method is called, we simply increase the age of this student by 1 year. However, since this an instance method, we need to add a parameter to the function at the very beginning of our list of parameters, typically named self. This parameter is automatically added by Python whenever we call an instance method, and it is a reference to the current instance on which the method is being called. We’ll learn more about this later.

Therefore, instead of referencing the age variable directly, we are using self.age to access the attribute age in the current instance of the class. Whenever we want to access an attribute of an instance in the definition of a class, we must use the variable self (or whatever is the first argument of the method) and a dot in front of it; otherwise, Python may complain that the attribute is not defined or behave in unexpected ways.

The grade() method is a bit more complex. As parameters, it accepts a reference to the current instance named self, a number of credits, and the grade points earned for a class; it then must update the credits and gpa attributes based on that new information. To do this, it must first calculate the current number of grade points the student has earned based on the current GPA, then update those values and recalculate the GPA.

So, in short, whenever we create an instance method while defining a class in Python, we’ll always need to remember to add an extra parameter—conventionally named self—to the beginning of our list of parameters so that we can always have a reference to the current instance of the class.

Note, no argument is passed for the self parameter when calling an instance method. To add a grade to the Student object Bob, the syntax for calling grade() is Bob.grade(3, 9).

Adding Class methods

Let’s add some class methods. In the class Main, we note that it has a class-attribute students which is a list of Student objects. We’ll add a main method, create a student and add it to the list of students.

from Student import *

class Main:
  students = []
  teachers = []

  @classmethod
  def main(cls, args):
    student1 = Student()
    cls.students.append(student1)
    # change name
    cls.students[0].name = "Mari"
    print(cls.students[0].name)
    #cls.students - a list of student-objects
    #cls.students[0] - a student-object stored at index 0
    #cls.students[0].name - the name attribute of a student-object stored at index 0

Here we create a student-object, add it to the class-list of students by using cls.students, then change its name.

Variable Scope

We’ve already discussed variable scope earlier in this course. Recall that two different functions may use the same local variable names without affecting each other because they are in different scopes.

The same applies to classes. A class may have an attribute named age, but a method inside of the class may also use a local variable named age. Therefore, we must be careful to make sure that we access the correct variable, using the self reference if we intend to access the attribute’s value in the current instance. Here’s a short example:

class Test:
  age = 15
  
  def foo(self):
    age = 12
    print(age)      # 12
    print(self.age) # 15
    
  def bar(self):
    print(self.age) # 15
    print(age)      # NameError

As we can see, in the method foo() we must be careful to use self.age to refer to the attribute, since there is another variable named age declared in that method. However, in the method bar() we see that age itself causes a NameError since there is no other variable named age defined in that scope. We have to use self.age to reference the attribute.

So, we should always get in the habit of using self to refer to any attributes, just to avoid any unintended problems later on.

Let’s go ahead and add the promotion() method to the Teacher class as well. That method should accept a single integer as a parameter (along with a parameter named self as the first parameter, of course), and then add that value to the Teacher’s current salary.

Use the first test below to check that we’ve included the correct methods in the Student and Teacher classes and main in Main. Then, use the second test to confirm that those methods work correctly. Each test below is worth 5 points in this module, for a total of 10 points.

Subsections of Methods

Instantiation

YouTube Video

Video Materials

Once we have created our class definition, complete with attributes and methods, we can then use those classes in our programs. To create an actual object based on our class that we can store in a variable, we use a process called instantiation.

UML Class Diagram showing Main, Student, and Teacher Classes, Attributes, and Methods UML Class Diagram showing Main, Student, and Teacher Classes, Attributes, and Methods

Instantiation

First, we’ll need to import each file that contains our other classes. For this project, we have code in both the Student.py and Teacher.py files, which are stored in the same directory as Main.py. So, in Main.py, we can use the following lines of code to import everything from Student.py and Teacher.py:

from Student import *
from Teacher import *

In that code, Student and Teacher refer to the Python files with those names, not the classes that are contained within them. It can be a bit confusing at first, and gets even more confusing when we start working with larger modules, but after working with it a few times it will become very familiar.

Next, we’ll need to build the structure for the Main class and main method, as well as the main guard.

However, we can use the information we’ve already learned to build these items pretty easily:

import sys
from Student import *
from Teacher import *

class Main:
  students = []
  teachers = []

  @classmethod
  def main(cls, args):

if __name__ == "__main__":
  Main.main(sys.argv)

In this code, we’ve created a class called Main, which has a method called main that we’ll use to start our program. Finally, we’ve added a standard main guard at the end. Notice that the main guard is not indented, so it is not part of the Main class itself.

Once we have imported all of our files and built our structure, we can use those classes in our code. To instantiate an object in Python, we basically call the name of the class like a function:

import sys
from Student import *
from Teacher import *

class Main:
  students = []
  teachers = []

  @classmethod
  def main(cls, args):
      Student()

if __name__ == "__main__":
  Main.main(sys.argv)

Of course, that will create a Student object, but it won’t store it anywhere. To store that object, we can create a new variable to which to assign the Student object we created:

import sys
from Student import *
from Teacher import *

class Main:
  students = []
  teachers = []
    
  
  @classmethod
  def main(cls, args):
      jane = Student()

if __name__ == "__main__":
  Main.main(sys.argv)

This will create a new Student object, and then store it in a variable of type Student named jane. While this may seem a bit confusing at first, it is very similar to how we’ve already been working with variables of types like int and float.

Accessing Attributes

Once we’ve created a new object, we can access the attributes and methods of that object, as defined in the class from which it is created.

For example, to access the name attribute in the object stored in jane, we could use:

import sys
from Student import *
from Teacher import *

class Main:
  students = []
  teachers = []
     
  @classmethod
  def main(cls, args):
      jane = Student()
      jane.name

if __name__ == "__main__":
  Main.main(sys.argv)

Python uses what is called dot notation to access attributes and methods within instances of a class. So, we start with an object created from that class and stored in a variable, and then use a period or dot . directly after the variable name followed by the attribute or method we’d like to access. Therefore, we can easily access all of the attributes in Student using this notation:

import sys
from Student import *
from Teacher import *

class Main:
  students = []
  teachers = []
    
  @classmethod
  def main(cls, args):
    jane = Student()
    
    jane.name
    jane.age
    jane.student_id
    jane.credits
    jane.gpa
    
if __name__ == "__main__":
  Main.main(sys.argv)

We can then treat each of these attributes just like any normal variable, allowing us to use or change the value stored in it:

import sys
from Student import *
from Teacher import *

class Main:
  students = []
  teachers = []
    
  @classmethod
  def main(cls, args):
    jane = Student()

    jane.name = "Jane"
    jane.age = jane.age + 15
    jane.student_id = "123" + "456"
    jane.credits = 45
    jane.gpa = jane.gpa - 1.1

    print(jane.name + ": " + jane.student_id)
    
if __name__ == "__main__":
  Main.main(sys.argv)

Accessing Methods

We can use a similar syntax to access the methods in the Student object stored in jane:

import sys
from Student import *
from Teacher import *

class Main:
  students = []
  teachers = []
      
  @classmethod
  def main(cls, args):
    jane = Student()

    jane.birthday()
    jane.grade(4, 12)
    
if __name__ == "__main__":
  Main.main(sys.argv)

Try It

Let’s see if we can use what we’ve learned to instantiate a new student and teacher object in our Main class. First, let’s look at the UML diagram once again:

UML Class Diagram showing Main, Student, and Teacher Classes, Attributes, and Methods UML Class Diagram showing Main, Student, and Teacher Classes, Attributes, and Methods

In that diagram, we see that the Main class should include a method called new_student(), which accepts several parameters corresponding to the attributes in Student. That method should also return an object of type Student. Similarly, there is a method called new_teacher() that does the same for the Teacher class.

So, let’s implement the new_teacher() method and see what it would look like:

  @classmethod
  def new_teacher(cls, name, focus, salary):
    some_teacher = Teacher()
    some_teacher.name = name
    some_teacher.focus = focus
    some_teacher.salary = salary
    cls.teachers.append(some_teacher)
  

Since new_teacher is a class method, we start with the decorator @classmethod; then start our function definition with the def keyword and the name of the function, followed by our list of parameters.

Inside the function, we instantiate a new Teacher object, storing it in a variable named some_teacher, to distinguish it from the class Teacher.

Then, we set the attributes in some_teacher to the values provided as arguments to the function. Finally, once we are done, we can add some_teacher to the class list of teachers. Let’s fill in both the new_teacher() and new_student() methods in the Main class now. We can use the buttons below to confirm that they work correctly.

A Word on Variable Names

In many code examples, it is very common to see variable names match the type of object that they store. For example, we could use the following code to create both a Teacher and Student object, storing them in teacher and student, respectively:

teacher = Teacher()
student = Student()

This is allowed in Python, since both data type names and variable identifiers are case-sensitive. Therefore, Teacher and teacher can refer to two different things. For some developers, this becomes very intuitive.

However, many other developers struggle due to the fact that these languages are case-sensitive. It is very easy to either accidentally capitalize a variable or forget to capitalize the name of a class.

So, in this course, we generally won’t have variable names that match class names in our examples. You are welcome to do so in your own code, but make sure you are careful with your capitalization!

Subsections of Instantiation

Constructors

On the last page, we created functions in the Main class to instantiate a Student and Teacher object and set the attributes for each object. However, wouldn’t it make more sense for the Student and Teacher classes to handle that work internally?

Thankfully, we can do just that by providing a special type of method in our classes, called a constructor method.

Constructors

A constructor is a special method that is called whenever a new instance of a class is created. It is used to set the initial values of attributes in the class. We can even accept parameters as part of a constructor, and then use those parameters to populate attributes in the class.

Technically, in Python one initializes an object by calling the initializer. The differences between constructors and initializers are arcane and deal with how and when memory is allocated on the heap; your computer, no kidding, has a has a heap of memory. We are going to use and stick to the much more standard "constructor" terminology.

Let’s go back to the Student class example we’ve been working on and look at our “constructor” for that class:

class Student:
  def __init__(self):
    self.name = "name"
    self.age = 19
    self.student_id = "123456987"
    self.credits = 0
    self.gpa = 0.0
  
  # other methods omitted

In this way, we can enforce the default attributes and values through the default constructor, without including them in the class declaration above, making the code a bit easier to read.

With that constructor, we can then instantiate the class and see that the attribute values are set correctly:

from Student import *

class Main:

  ...   
  def main(cls, args):
    some_student = Student()
    print(some_student.name) # "name"
    print(some_student.age)  # 19
  ...

So, constructors are a very easy way to help set initial values for attributes in a class. We can even call class methods directly from the constructor if needed to help with setup.

Constructors with Parameters

We can also create constructors that accept arguments. For example, we can create a constructor that accepts a value for each attribute as in this example:

class Student:
  
  def __init__(self, name, age, student_id, credits, gpa):
    self.name = name
    self.age = age
    self.student_id = student_id
    self.credits = credits
    self.gpa = gpa

  # other methods omitted

Inside that constructor, the code looks very similar to the function we added to the Main class on the previous page. It will use each parameter to set the corresponding attribute, using the self variable once again to refer to the current object.

However, this constructor will require us to include a value for each parameter in order to instantiate an object. What if we want to be able to instantiate an object without providing all of these values? To do that, we can include default values for each parameter, allowing us to call the constructor with some or all parameters omitted. Let’s look at an example:

class Student:
  
  def __init__(self, name="name", age=19, student_id="123456987", credits=0, gpa=0.0):
    self.name = name
    self.age = age
    self.student_id = student_id
    self.credits = credits
    self.gpa = gpa

  # other methods omitted

This example will assign a value to each attribute using the values provided as arguments to the constructor, or else the default values if some arguments are omitted.

When instantiating an object with a constructor like this one, we can use keyword arguments to specify which parameters we are including, as shown below:

a_student = Student(name="Jane", credits=15)

So, by including default values for each parameter in our constructor, we can create a very convenient way to instantiate our object containing attributes populated by any combination of default values and values provided by our code.

UML Class Diagrams

We can also add constructors to our UML class diagrams. A constructor is usually denoted in the methods section of a class diagram by using the __init__ method name and omitting the return type. In addition, they are usually included at the top of the methods section, to make them easier to find:

UML Class Diagram with Constructors UML Class Diagram with Constructors

The diagram above shows the Student and Teacher classes with constructors included.

For UML not written specifically for Python, you will generally see the “constructor” syntax instead of an __init__ method. This syntax is a method with the class name as a method, so a Student() method in the Student class diagram.

Try It!

Now that we know how to use constructors, let’s modify our working example to add the following constructors to both Teacher and Student:

  • A constructor that accepts a parameter for each attribute, and uses those values to populate the object. Each parameter should be given a default value such that the constructor can optionally be called with no arguments or keyword arguments.

In Main change new_student and new_teacher so they call the constructor with the proper arguments– hint each should contain 2 -lines, a call to the appropriate constructor and a line adding the new object to the correct list.

The example for Student has already been created above, but we’ll have to figure out how to do the same for the Teacher class.

Subsections of Constructors

Call by Reference

Finally, it is important to remember that any instantiated objects used as arguments to a method are passed in a call-by-reference manner. So, any modifications to those objects made inside of a method will also be reflected in the code that called the method.

Here’s a quick example:

class Reference:
  
  def __init__(self):
    self.x = 0
from Reference import *

class Main:
  
  @staticmethod
  def main():
    some_ref = Reference()
    some_ref.x = 10
    Main.modify(some_ref)
    print(some_ref.x)  # 15
    
  def modify(a_ref):
    a_ref.x = 15
    
if __name__ == "__main__":
    Main.main()

As we can see, when we call the modify() function and pass a Reference object as an argument, we can modify the attributes inside of that object and see those changes back in the main() method after modify() is called.

Of course, if we reassign the argument’s variable to a new instance of Reference inside of the modify() function, then we won’t see those changes in main() because we are dealing with a newly created object.

Call by Object Reference

In many examples, Python exhibits behavior very similar to “call-by-reference” as discussed in other programming languages. However, because Python allows both mutable and immutable objects, there are some subtle differences in how it handles objects stored in variables compared to languages like Java and C++.

One developer has proposed the term call-by-object-reference to help clarify the difference. You can read more on his blog.

For now, we’ll continue to use the term call-by-reference since it has a standard usage across many programming languages. When in doubt, it is always a good idea to write a simple test case and make sure the program you are developing works as expected.

So, we’ll need to keep this in mind as we use objects as parameters and returned values in any methods we create in our programs.

Properties

YouTube Video

Video Materials

So far in this chapter we’ve learned how to create private and public attributes in our classes. What if we want to create an attribute that is read-only, or one that only accepts a particular set of values? In Python, we can use a special decorator @property to define special methods, called getters and setters, that can be used to access and update the value of private attributes.

Getter

In Python, a getter method is a method that can be used to access the value of a private attribute. To mark a getter method, we use the @property decorator, as in the following example:

class Property:
  
  def __init__(self):
    self.__name = ""    # create empty private attribute
    
  @property
  def name(self):
    return self.__name

In this class, the name attribute is private, so normally we wouldn’t be able to access its value. However, we’ve created a method name() that acts as a getter for the name private attribute. The decorator @property allows us to use that method as an attribute. In this way, the value of that variable can be accessed in a read-only fashion.

From other code, we can call that method by treating it just like an attribute called name:

prop = Property()
name = prop.name

Setter Methods

Similarly, we can create another method that can be used to update the value of the name attribute:

class Property:
  
  def __init__(self):
    self.__name = ""    # create empty private attribute
    
  @property
  def name(self):
    return self.__name
  @name.setter
  def name(self, value):
    if not isinstance(value, str):
      raise ValueError("Name must be a string")
    if len(value) == 0:
      raise ValueError("Name cannot be an empty string")
    self.__name = value

In this code, we’ve added another name() method below the decorator @name.setter that can be used to update the value stored in the name attribute. We’re also checking to make sure that the argument provided to the value parameter is a string, and is not an empty string. If it is, we can raise a ValueError, which would alert the user that this is not allowed. Of course, it would be up to the person writing the code that calls this method to properly catch and handle this exception.

In our other code, we can then update that value just like we would any other attribute:

prop = Property()
prop.name = "test"

UML Class Diagrams

Getter and setter methods are displayed on a UML class diagram just like any other method. We use naming conventions such as name() and name(value: str) to make it clear that those methods are getters and setters for the attribute name of type str, as in this UML class diagram:

UML Class Diagram with Properties UML Class Diagram with Properties

Try It!

So, through the use of getter and setter methods, along with the @property decorators, we can either prevent other code from updating an attribute, or enforce restrictions on that attribute’s values, without actually exposing the attribute. Here’s a sample main class that demonstrates how to use these properties:

from Property import *

class Main:
  
  @staticmethod
  def main():
    prop =  Property()
    name = prop.name
    print(name)
    prop.name = "test"
    print(prop.name)
    
    
if __name__ == "__main__":
    Main.main()

Subsections of Properties

A Worked Example

Now that we’ve learned all about how to make our own classes and objects, we can practice our skills by building an example program together. This will be a larger program than many of the programs we’ve worked with so far, but hopefully it will actually be easier to follow since the code is separated into several classes.

Problem Statement

For this example, we’ll build a program to play a version of the game of Blackjack, also known as Twenty-One. The rules of this simplified game are fairly straightforward:

  1. The game consists of a single player playing against the dealer, played by our program in this example.
  2. Each player is initially dealt two cards from a standard 52 card deck. Each card’s value is its face value, with face cards valued 10 and aces valued 11.
  3. The object of the game is to get a higher total value than the other player, without going over 21.
  4. The game consists of several steps:
    1. Both the player and the dealer review the two cards they are dealt. Both the player and the dealer can see the opponent’s hand as well.
    2. The player is given the option to draw additional cards. The player may continue to draw cards until she or he chooses to stop, or their total value is greater than 21.
    3. If the player stops before going over 21, the dealer must draw cards to try to beat the player. The dealer stops drawing cards when the dealer’s total beats the player’s or exceeds 21.
    4. At the end, the participant with the greatest card value that is less than or equal to 21 wins the game. If it is a tie, the dealer wins. As you may be able to tell, this game differs a bit from the rules of traditional Blackjack. Those changes mainly help to simplify the program a bit, so we can focus on the structure of the classes we need to build instead of on the rules.

You can find the full rules to Blackjack on Wikipedia.

Program Structure

In order to build this program, we’ll need to implement several classes to represent the objects needed for the game. For now, we’ll follow this UML diagram to help guide the design of this program. In later chapters, you’ll learn the skills needed to design your own program structures from scratch, but when learning to program it is sometimes easier first to read different program structures before writing your own.

Blackjack UML Diagram Blackjack UML Diagram

This program will contain several classes:

  • Card—This class represents a single card in a deck, containing a suit and a value.
  • Deck—This class represents the entire deck of cards, consisting of 52 cards.
  • Hand—This class represents a single player’s hand of cards from the deck.
  • Dealer—This class implements the dealer’s actions.
  • Player—This class implements the player’s actions.
  • Main—This class controls the program and contains the main() function.

To build this program, we’ll address each class individually, allowing us to build the program one piece at a time and test it at each step to make sure it works correctly.

Card Class

YouTube Video

Video Materials

The first and simplest class we can build is the Card class. This class represents a single card from a deck of cards, and contains the suit, name, and value attributes. Since we don’t want those values to be edited outside of this class, we can use private attributes paired with getter methods for them. For the value, we’ll use an integer to make the rest of the program simpler. We’ll also need to create a simple constructor for this class. It can accept a suit and a card number as input, and then populate the attributes as needed:

class Card:
  
  def __init__(self, a_suit, a_number):
    self.__suit = a_suit
    if a_number == 1:
      self.__name = "Ace"
      self.__value = 11
    elif a_number == 11:
      self.__name = "Jack"
      self.__value = 10
    elif a_number == 12:
      self.__name = "Queen"
      self.__value = 10
    elif a_number == 13:
      self.__name = "King"
      self.__value = 10
    else:
      self.__name = str(a_number)
      self.__value = a_number

  @property
  def suit(self):
    return self.__suit
    
  @property
  def name(self):
    return self.__name
    
  @property
  def value(self):
    return self.__value

Finally, we can add some additional code to our constructor to validate the supplied parameter arguments, just to avoid any unforeseen errors. In this case, we’ll make sure that the suits and numbers provided are all valid values:

class Card:
  
  def __init__(self, a_suit, a_number):
    if not (a_suit == "Spades" or a_suit == "Hearts" or a_suit == "Clubs" or a_suit == "Diamonds"):
     raise ValueError("The suit must be one of Spades, Hearts, Clubs, or Diamonds")
    if a_number < 1 or a_number > 13:
      raise ValueError("The card number must be an integer between 1 and 13, inclusive")
    self.__suit = a_suit
    if a_number == 1:
      self.__name = "Ace"
      self.__value = 11
    elif a_number == 11:
      self.__name = "Jack"
      self.__value = 10
    elif a_number == 12:
      self.__name = "Queen"
      self.__value = 10
    elif a_number == 13:
      self.__name = "King"
      self.__value = 10
    else:
      self.__name = str(a_number)
      self.__value = a_number

  @property
  def suit(self):
    return self.__suit
    
  @property
  def name(self):
    return self.__name
    
  @property
  def value(self):
    return self.__value

To do this, we’ve added a couple of If-Then statements to the constructor that can raise Exceptions if the inputs are invalid.

Finally, to make debugging our programs very simple, we’ll add a special method, __str__() to this class. The __str__() method is actually a part of every class in Python because of inheritance, something we’ll learn more about in a later chapter. For now, we can add that method as shown below:

class Card:
  
  # other code omitted here #
  
  def __str__(self):
    return "{} of {}".format(self.__name, self.__suit)

Later, when we try to print a Card class, Python will automatically call the __str__() method behind the scenes to convert the Card class to a String that can be easily understood by our users.

That should complete the Card class! The assessments below will confirm that the code structure and functionality is correct before moving on.

Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

Deck Class

YouTube Video

Video Materials

Next, we’ll need a class that can represent an entire deck of cards. This class will contain a list of cards, as well as some helpful methods we can use to shuffle the deck and deal individual cards.

In this class, we won’t include any getters or setters for the deck of cards itself because our program shouldn’t be able to change the deck in any way. In the constructor, however, we’ll include the code to create our deck of cards:

from Card import *

class Deck:
  
  def __init__(self):
    self.__card_deck = []
    suits = ["Spades", "Hearts", "Diamonds", "Clubs"]
    for suit in suits:
      for i in range(1, 14):
        self.__card_deck.append(Card(suit, i))

Hopefully that code is pretty straightforward. It creates an empty list that will later contain the cards, then a list of strings representing the suits. Finally, we have two For loops to go through each suit and each card number from 1 to 13, creating the full deck of 52 cards.

Next, we can add a method to print out the entire deck of cards. Once again, we’ll just use the __str__() method:

from Card import *

class Deck:
  
  def __init__(self):
    self.__card_deck = []
    suits = ["Spades", "Hearts", "Diamonds", "Clubs"]
    for suit in suits:
      for i in range(1, 14):
        self.__card_deck.append(Card(suit, i))

  def __str__(self):
    output = ""
    for a_card in self.__card_deck:
      output += str(a_card) + "\n"
    return output

At this point, let’s test it out! That’s one of the most important steps in writing programs like this one—we’ll want to test each little bit of the program and see how it works.

So, we can add the following code to our Main class for testing:

from Deck import *

class Main:

  @staticmethod
  def main():
    a_deck = Deck()
    print(a_deck)
    
if __name__ == "__main__":
  Main.main()

Then, we can run that code using these commands in the terminal:

cd 11p-classes/example
python3 Main.py

When we run those commands, we should see output similar to this:

Terminal Output with Ordered List of Cards Terminal Output with Ordered List of Cards

As we can see, we are creating a full deck of cards, but they aren’t in a random order. While we could just implement a method to draw cards randomly from the deck, it might be just as useful to implement a method to shuffle the deck. So, let’s do that now!

import random
from Card import *

class Deck:
  
  # other methods omitted here #
  
  def shuffle(self, times):
    if times <= 0:
      raise ValueError("The deck must be shuffled a positive number of times")
    for i in range(0, times):
      first = random.randint(0, 51)   # get a number [0...51]
      second = random.randint(0, 51)  # get a number [0...51]
      if first != second:  # swap first and second cards
        temp = self.__card_deck[first]
        self.__card_deck[first] = self.__card_deck[second]
        self.__card_deck[second] = temp

This is a very simple shuffle method, which simply gets two random numbers using the random library. Then, it will swap the cards in the deck at those two locations. It is slow and simple, but thankfully a computer can do thousands of those operations in a few milliseconds, so it works just fine for our needs.

Now, we can update the code in our main() function to see that it is working correctly:

from Deck import *

class Main:

  @staticmethod
  def main():
    a_deck = Deck()
    a_deck.shuffle(1000)
    print(a_deck)
    
if __name__ == "__main__":
  Main.main()

That should produce a random deck, as shown in this screenshot:

Terminal Output with Shuffled List of Cards Terminal Output with Shuffled List of Cards

Finally, we can add a method to deal a card from the deck to a player. To do this, we’ll add another private attribute to keep track of the position we are dealing from in the deck. Then, we can return the appropriate card from our method.

import random
from Card import *

class Deck:
  
  # other methods omitted here #
  
  def __init__(self):
    self.__card_position = 0
    # other constructor code omitted here #
    
  def draw(self):
    output = self.__card_deck[self.__card_position]
    self.__card_position += 1
    return output

There we go! This method will simply return the front-most card from the deck that hasn’t already been used. We aren’t actually removing the cards from the list, but rather just incrementing a variable keeping track of the position in the list to remember which cards we’ve already dealt.

That should complete the Deck class! The assessments below will confirm that the code structure and functionality is correct before moving on.

Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

Hand Class

YouTube Video

Video Materials

Next, we can create a simple class that represents a hand of cards. So, it will need a list of Card objects just like the Deck class.

To start, we can create our list of cards in the constructor, as well as an integer to keep track of how many cards we have in our hand:

from Card import *

class Hand:
  
  def __init__(self):
    self.__card_hand = []

Then, we can also create a value() method to return the value in our hand, as well as a __str__() method to print out the contents of our hand:

from Card import *

class Hand:
  
  def __init__(self):
    self.__card_hand = []
    
  @property
  def value(self):
    value = 0
    for card in self.__card_hand:
      value += card.value
    return value
  
  def __str__(self):
    output = ""
    for card in self.__card_hand:
      output += str(card) + "\n"
    return output

These methods are very similar to each other. In essence, we use a For Each loop to loop through the cards in our hand, and then either sum up the values or get the string representation of each card.

Lastly, we need to create a method that allows us to add a card to our hand. So, we can implement the addCard() method as well:

from Card import *

class Hand:
  
  # other methods omitted here #
  
  def add_card(self, input):
    if not isinstance(input, Card):
      raise ValueError("Input must be a Card object")
    self.__card_hand.append(input)

That should do it for the Hand class. The assessments below will confirm that the code structure and functionality is correct before moving on.

Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

Dealer Class

YouTube Video

Video Materials

Now that we have implemented the classes needed to keep track of the cards, we can create the classes that will actually perform the actions for each player. First, we can implement the code for the Dealer class. This class is actually pretty simple, since it will only consist of a couple of methods: make_moves(), which will perform all the actions needed for the dealer, and a __str__() method to print the contents of the dealer’s hand. In addition, the dealer will need an attribute to store a hand, which can be populated in the constructor by a parameter:

from Hand import *

class Dealer:
  
  def __init__(self, a_hand):
    if not isinstance(a_hand, Hand):
      raise ValueError("A_hand must be a Hand object")
    self.__my_hand = a_hand
    
  def make_moves(self, player_value):
    while self.__my_hand.value < player_value and self.__my_hand.value <= 21:
      # we need to draw a card here!
  
  def __str__(self):
    output = "The dealer currently holds: \n"
    output += str(self.__my_hand)
    output += "for a total of {}".format(self.__my_hand.value)
    return output

At this point, we notice a flaw in our design! The Dealer class needs to be able to draw a card from the deck, but we’ve provided no way for it to do so. In addition, we’ll need to do the same thing for our Player class as well. So, how can we accomplish this?

Option 1: Make Deck Static

One way we can accomplish this is to make the methods in our Deck class static. In this way, we can access them from anywhere in the program, even without having access to a Deck object. For this, we’d modify our Deck class to look similar to this:

import random
from Card import *

class Deck:
  __card_deck = []
  __card_position = 0
  
  # __init__ changed to init
  @staticmethod
  def init():
    suits = ["Spades", "Hearts", "Diamonds", "Clubs"]
    for suit in suits:
      for i in range(1, 14):
        Deck.__card_deck.append(Card(suit, i))
  
  @staticmethod
  def draw():
    output = Deck.__card_deck[Deck.__card_position]
    Deck.__card_position += 1
    return output

  @staticmethod
  def shuffle(times):
    if times <= 0:
      raise ValueError("The deck must be shuffled a positive number o times")
    for i in range(0, times):
      first = random.randint(0, 51)   # get a number [0...51]
      second = random.randint(0, 51)  # get a number [0...51]
      if first != second:  # swap first and second cards
        temp = Deck.__card_deck[first]
        Deck.__card_deck[first] = Deck.__card_deck[second]
        Deck.__card_deck[second] = temp
  
  # __str__ changed to str
  @staticmethod
  def str():
    output = ""
    for a_card in Deck.__card_deck:
      output += str(a_card) + "\n"
    return output

In the code above, all of the methods and instance attributes are now static. In addition, the constructor was renamed to init() since we won’t actually be using the constructor to build an object. Finally, we also have to rename the __str__() method to str() since the __str__() method cannot be static. We’ll see why that would be a problem in a later chapter as we learn about method inheritance.

Then, once we’ve made that choice, we can draw a new card from the deck in our code anywhere simply by using the Deck.draw() method.

This approach has several advantages and disadvantages. It requires very few changes in our code, and once the Deck class is modified, we can use the methods from that class anywhere in our code, which can be very helpful.

Unfortunately, the major downside of this approach is that we can now only have a single deck of cards in our entire game. For most games, that won’t be an issue, but it could be a limitation in other programs. In addition, as we may learn in a later class, there are some standard ways to accomplish this, such as the singleton design pattern, that are more familiar to developers.

So, for this example, we won’t be using a Deck class that contains entirely static methods and attributes.

Option 2: Pass Deck Object to Dealer Class

The second way this could be handled is to create an object from the Deck class in our Main class, then pass that object as a parameter to the constructor for the Dealer object. In that way, the dealer has a reference to the deck that is stored in our main class (recall that all objects are handled in a call by reference manner).

So, we can update the constructor for our Dealer class to accept a Deck object, and then we’ll store it as a private variable in the class:

from Hand import *
from Deck import *

class Dealer:
  
  def __init__(self, a_hand, a_deck):
    if not isinstance(a_hand, Hand):
      raise ValueError("A_hand must be a Hand object")
    if not isinstance(a_deck, Deck):
      raise ValueError("A_deck must be a Hand object")
    self.__my_hand = a_hand
    self.__the_deck = a_deck
    
  def make_moves(self, player_value):
    while self.__my_hand.value < player_value and self.__my_hand.value <= 21:
      new_card = self.__the_deck.draw()
      print("The dealer draws a {}".format(str(new_card)))
      self.__my_hand.add_card(new_card)
  
  def __str__(self):
    output = "The dealer currently holds: \n"
    output += str(self.__my_hand)
    output += "for a total of {}".format(self.__my_hand.value)
    return output

Then we can place this code in our Main class:

from Deck import *
from Hand import *
from Dealer import *

class Main:

  @staticmethod
  def main():
    the_deck = Deck()
    the_deck.shuffle(1000)
    dealer_hand = Hand()
    dealer_hand.add_card(the_deck.draw())
    dealer_hand.add_card(the_deck.draw())
    a_dealer = Dealer(dealer_hand, the_deck)
    
if __name__ == "__main__":
  Main.main()

With this code, we are instantiating a single Deck object in our Main class, then passing it as a reference to the Dealer class when we instantiate it. This gives the Dealer class a reference to the deck that we are using, allowing it to draw cards as needed.

This approach also has several pros and cons. First, we don’t have to modify our Deck class at all, meaning that it can remain a simple object. Instead, we are modifying how we use it by including a reference to it in our other classes. This is a more standard approach for programs written in an object-oriented style, in Python and other languages.

As a downside, this does mean that we’ll have to make sure our other classes all are given a reference to a Deck object. In larger programs, handling all of these object references can become very cumbersome and time-consuming. Again, in a future course we can learn about design patterns that help simplify this process, too.

We’ll use this approach in our example program here.

That should complete the Dealer class! The assessments below will confirm that the code structure and functionality is correct before moving on.

Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

Player Class

YouTube Video

Video Materials

The player class is nearly identical to the Dealer class. The only difference is that the player class will ask the player to decide whether to draw more cards. In addition, the player may draw until their value is greater than 21, without regard to the score from the dealer. This is the one interactive part of the entire program:

import sys
from Hand import *
from Deck import *

class Player:
  
  def __init__(self, a_hand, a_deck):
    if not isinstance(a_hand, Hand):
      raise ValueError("A_hand must be a Hand object")
    if not isinstance(a_deck, Deck):
      raise ValueError("A_deck must be a Hand object")
    self.__my_hand = a_hand
    self.__the_deck = a_deck
    
  def make_moves(self):
    with sys.stdin as reader:
      while self.__my_hand.value <= 21:
        print("You currently have a value of {}".format(self.__my_hand.value))
        print("Would you like to draw another card (y/n)?: ")
        input = reader.readline().strip()
        if input == "y" or input == "Y":
          new_card = self.__the_deck.draw()
          print("The dealer draws a {}".format(str(new_card)))
          self.__my_hand.add_card(new_card)
        elif input == "n" or input == "N":
          break;
        else:
          print("Invalid input!")
      print("You end your turn with a value of {}".format(self.__my_hand.value))
  
  def __str__(self):
    output = "The player currently holds: \n"
    output += str(self.__my_hand)
    output += "for a total of {}".format(self.__my_hand.value)
    return output

As we can see in the make_moves() method, we’ve simply added the code to create a reader object to handle user input. Then, we can ask the user at each step whether they would like to draw another card.

That should complete the Player class! The assessments below will confirm that the code structure is correct before moving on. We won’t worry about testing the functionality here, since that is really best done by a live player!

Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

Main Class

YouTube Video

Video Materials

Finally, we can work on implementing our Main class. This class is very simple, only containing the main() method for the program. The main method will set up the deck and deal a hand for each player, then allow both the player and the dealer to make moves before finally getting the result to see who wins:

from Deck import *
from Hand import *
from Dealer import *
from Player import *

class Main:

  @staticmethod
  def main():
    the_deck = Deck()
    
    print("Shuffling the deck...")
    the_deck.shuffle(1000)
    
    print("Dealing the player's hand...")
    player_hand = Hand()
    player_hand.add_card(the_deck.draw())
    player_hand.add_card(the_deck.draw())
    a_player = Player(player_hand, the_deck)
    print(a_player)
    
    print("Dealing the dealer's hand...")
    dealer_hand = Hand()
    dealer_hand.add_card(the_deck.draw())
    dealer_hand.add_card(the_deck.draw())
    a_dealer = Dealer(dealer_hand, the_deck)
    print(a_dealer)
    
    print("Starting player's turn...")
    a_player.make_moves()
    print(a_player)
    
    print("Starting dealer's turn...")
    a_dealer.make_moves(player_hand.value)
    print(a_dealer)
    
    if player_hand.value <= 21 and dealer_hand.value > 21:
      print("The player wins!")
    elif player_hand.value <= 21 and player_hand.value > dealer_hand.value:
      print("The player wins!")
    elif dealer_hand.value <= 21:
      print("The dealer wins!")
    else:
      print("There is no winner")
    
if __name__ == "__main__":
  Main.main()

Looking at this code, we see that the main method consists of several steps:

  1. First, the deck is initialized and shuffled.
  2. Then, the player’s hand is dealt, and the Player object is initialized. Once that is done, it prints the contents of the player’s hand.
  3. Similarly, the Dealer is initialized and given a hand, the contents of which are printed.
  4. Then, it proceeds to the player’s turn, on which the player can draw cards until he chooses to stop or go over the value of 21.
  5. Next, the dealer is given a turn. The dealer is also given the value of the player’s hand, so the dealer only has to match or beat the player’s value to win.
  6. Finally, the game determines the winner.

Here is a sample of this program’s output when run in the terminal:

Python Program Output Python Program Output

There we go! We were able to build a program that plays a simple version of Blackjack. While it took a bit of work to get all of the required classes developed, each one was pretty straightforward and easy to use. Then, in our main() method, we simply had to pull all those resources together into a working program.

That’s one of the major benefits of the object-oriented programming paradigm. Once the program can be expressed by the objects and interactions required, the bulk of the actual logic code is simply the process of building and manipulating those objects in a way that feels very similar to how it would be done in the real world.

The assessments below will verify that the entire example program works as intended, except for the Main and Player classes. This is because it can be very difficult to complete those classes in a way that is easy to test automatically. They exist mainly to allow us to interact with the objects we’ve created.

Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

Subsections of A Worked Example