Class Objects Motivating Example

Suppose we again place ourselves in the role of a geography teacher. It’s now the end of the school year, and we want to examine the grades of the students. The following table contains the grades for each student and each percentage of the final grade it represents.

Student Homework Total (20%) Project 1 (10%) Midterm (20%) Project 2 (10%) Final (40%)
Mary 85 90 88 100 90
Matthew 80 95 70 90 70
Marie 90 92 84 0 91
Manuel 79 70 85 70 82
Malala 100 95 100 98 99

Now what we want to do is figure out some summary statistics about each student in the class. To do this, we will use a new form of storing data and methods called a class object. With class objects, we can develop new and efficient algorithms for completing tasks.


Introduction to Python Objects

Throughout the past few tutorials, we’ve described various Python “things,” such a data types, structures, and functions as “objects.” We also introduced in the first tutorial that Python is an “object-oriented” scripting language. The subject of object-oriented programming is broad, and this tutorial will not attempt to cover its entirety. However, we will cover the basics of object-oriented programming as they relate to Python, and we’ll cover the class constructor in detail within this tutorial.

Can't get enough Python?

Enter your email address for more free Python tutorials and tips.

Python is powerful! Show me more free Python tips

Python Objects

Python objects are variables, classes, functions, and other “things” that maintain some pointer (where it is located), contain some data, and may contain internal procedures or methods. For example, let’s create a string object:

x = "string"  # Object instantiation and pointer assignment to x
id("string"), id(x)  # Verify that x points to the same object
> (3106053026736, 3106053026736)

When we use quotations, Python creates or “instantiates” the string object. If we want to locate where the object is in memory to use it, then we will need to create a “pointer” to the string object. We created the above pointer using the assignment operator = to assign the variable x as a pointer to the string object we created. We can tell that the x variable points to the same string object using the id function.


Object Attributes

An “attribute” of an object is some value or method that is attached to that object. In a sense, an attribute is a set of variables and functions that are defined within the “sandbox” of the object. When a function is an attribute of an object, it is called a “method.” Attributes of an object in Python are referenced using the object.attribute convention. We can look at all of the attributes associated with an object using the builtin dir() function1.

dir(x)
> ['__add__',
>  '__class__',
>  '__contains__',
>  '__delattr__',
>  '__dir__',
>  '__doc__',
>  '__eq__',
> ... # Omitting further results

We can see that a string object has quite a number of attributes associated with it, predominantly methods. These methods operate on the values within the object, which is the primary reason they are considered separate from functions; they already “know” information about the object.

For example, if I wanted to make all the letters in the string we created uppercase, then I can call the upper method :

x.upper
> <function str.upper>
x.upper()
> 'STRING'

As we mentioned previously in our tutorial on functions, its important to know that the method is referenced by x.upper, and is called or “activated” by x.upper().


Attribute Naming Conventions

Many beginners to Python can be confused by the confusing naming convention used by Python for object attributes. In the above example on strings, the string object had attributes x.__dir__, and x.upper. Why were the underscores added to the dir object and not the upper object? The reason is due to Python’s convention for classifying attributes of an object.

The following table gives a summary of this convention for an example attribute atr:

Name Example Reason
Single Leading Underscore _atr Declaring "private" variables, or internal use only
Single Trailing Underscore atr_ Avoids conflict with builtin keywords
Double Leading Underscore __atr Attribute name "mangling"
Double Leading and Trailing Underscore __atr__ Special internal Python use

Single leading underscores are used to declare that attributes within an object are “private” 2 and should not be used outside of the internal structure of the object. This type of declaration is used more in defining variables and functions within modules, which we will cover in a later tutorial.

Single trailing underscores are used to differentiate attribute names from other Python builtin names. For example, if I wanted to make a method named print, but did not want to call the builtin Python function print, then I could name the attribute print_.

Double leading underscores indicate to Python that the referenced name should be “mangled” or changed using the class name. For example, an attribute named __atr within an object can only be accessed outside of the object by object._ClassName__atr. We will cover these in more detail later.

Double leading and trailing underscores are dedicated to Python internal functions and processing. Like we saw above, the dir builtin function of Python works by accessing the object.__dir__ attribute. Because these attributes are primarily used by Python itself, they are rarely created by users.


Class Constructors

Now that we’ve talked about Python objects that already exist within Python, lets go over how we can create new objects for ourselves. We can define new class objects using the class object constructor. The class object constructor has the following format:

class [name]:
	[internals]

Like if statements and loops, indented lines (tab or 4 spaces) following the opening line are grouped within the constructor.

We can create a simple example class constructor with an attribute key using the following:

class example:
	key = "example string"

Note that objects attributes need not be setup in the class constructor, but can be dynamically added later. For example:

x = example()
x.addedvalue = 1000
print(x.addedvalue)
> 1000

Therefore we can store variables within class objects themselves.


Class Objects vs. Constructors

In the above example, we created a class constructor named example. A common mistake for beginners to Python is to assume that the class constructor is the class object. This is not the case. A constructor forms the template of a class object, on which class objects are created or instantiated. For example, if we create an example object by instantiating it and assigning it to the variable x, we can reference and manipulate the key attribute:

class example:
	key = "example string"
x = example()  # Instantiate class object and assign to x
print(x.key)
> example string
x.key = "object string"  # Changing the key attribute
print(x.key)
> object string
print(example.key)  # Note how the object attribute did not change the constructor attribute
> example string

Knowing the difference is important, as different objects of the same parent class can take different values:

x = example()
y = example()
x.key = "One example"
y.key = "A different example"
print(x.key)
> One example
print(y.key)
> A different example

As you can see from the above example, the example class created a class of object that contains the key attribute. Instantiating the object on two different variables allows us to change the key on each variable.

Can't get enough Python?

Enter your email address for more free Python tutorials and tips.

Python is powerful! Show me more free Python tips

Class Methods

We’ve previously mentioned that functions can be included as attributes of an object in what are called methods. We can do this simply by defining a function within the class constructor:

class example:
	key = "example string"
	def addup(self, a, b): # We will explain the "self" statement in a moment
		return(a + b)
x = example()  # object initialization
x.addup(3,2)
> 5

With a minor change in the input tuple, we’ve now recreated a function to sum two numbers as a method of an object. However, you may wonder why we would want to create this method instead of just creating a function. The secret lies within the self variable in the method input tuple.

Whenever a method is defined within a class object, the first input object will always be the self variable3. We can imagine that the self variable refers to the object itself. This means that any attribute defined as part of the object can be referenced within the object as self. For example:

class example:
	value = 3
	def addup(self, a):
		return(a + self.value) # We will now add a given value to the internal value
x = example()  # object initialization
x.addup(2)
> 5
x.value = 8  # We can change the internal value variable using the x.value attribute
x.addup(2)
> 10

In other words, when we call self within the class constructor, we are telling Python to use an attribute of the same object. Because we can instantiate the object on any variable (x, y, etc.) then self provides a consistent way to reference the object from within the constructor.


Special Methods for Class Objects

In our introduction section we mentioned that Python has reserved special object methods of the format __method__ for use by its builtin functions. In this section, we’ll go over a few of these special methods, and how they can be used to develop effective objects. There are many more special methods than what is presented here, but those methods require more knowledge of Python mechanics that we will discuss in future tutorials.


__init__ Method

The __init__ method is a standard method that Python uses when instantiating (or initializing) and object. Suppose we instantiate the following class object x = example(). When we call the constructor with example(), Python will search the object for the __init__ method and automatically execute its contents with the instantiating input tuple. For example:

class example:
	def __init__(self, input):  # Don't forget to put self first!
		self.key = input
print(example.key)  # Check to see if the constructor has a key. This should fail.
> AttributeError: type object 'example' has no attribute 'key'
x = example("sample text")  # Instantiate the class object and use the following input in __init__
print(x.key)
> sample text

The majority of class object usage involves __init__, as it’s used to setup the object for later use.


Iterators: __iter__ and __next__

In our previous tutorial on functions, we discussed generator functions and their uses at length. We will expand on the concept of a generator into the more general iterator class of object. Generator functions are a shortcut way of defining an iterator; however if a more complex process of iteration is required, then we can create an iterator.

The __init__ method in an object will tell Python that the object is iterable and callable with the iter function. For example, list objects possess an __iter__ method, and thus we can iterate over them using a for ... in ... statement.

The __next__ method tells Python what actions to perform when the next iteration call is performed on the object. This method provides the same capabilities of a while loop coupled with a yield statement for generators.

Let’s start by making our own iterator to print out a list of strings.

class myIterator:
	def __init__(self, input):  # __init__ to read in an input list
		self.listing = input
		self.max = len(input)  # start an index of 
		self.index = -1
	def __iter__(self):
		return(self)  # This simply points the object back to itself when called
	def __next__(self):  # The essential iteration loop
		self.index += 1 # Increment index by 1 on each loop
		if self.index >= self.max:  # If we've reached the end of the list of items
			raise StopIteration    # This will stop iteration after the list is exhausted
		return(self.listing[self.index])
		
listing = ["The", "best", "of", "times."]
x = myIterator(listing)
for item in x:
	print(item)
> The
> best
> of
> times.

Note in the above example, we maintained a condition within the __next__ method to raise an exception to halt execution of the iterator once all elements were exhausted. This is similar to the halt we added to a generator function to prevent infinite loops. Don’t worry for now about what the raise statement entails; we will discuss exceptions in detail in a later tutorial.


__dir__ Method

We’ve previously mentioned the dir function and how it examines an object for the __dir__ method to output a list of all available methods. We can setup this list ourselves within an object:

class example:
	def __init__(self):
		self.key = 1000
	def printkey(self):
		print(self.key)
	def __dir__(self):
		return(["__init__", "__dir__", "printkey"])
x = example()
dir(x)
> ['__dir__', '__init__', 'printkey']

Class Inheritance

Class objects support inheritance of attributes from other “parent” objects. When a class object is defined, a parent class or tuple of classes can be added by adding a call to the end of the class definition:

class exampleParent:
	mainKey = 1000
	
class exampleChild(exampleParent):
	childKey = 2
x = exampleChild()  #Note we only have to invoke the parent class within the constructor
print(x.mainKey)
> 1000
print(x.childKey)
> 2

Notice how in the above example, the exampleChild object did not itself define the mainKey attribute, but rather it “inherited” the attribute from the parent class. Inheritance is especially useful in situations where multiple classes use the same methods. Instead of re-typing those methods within each class, you can define a parent class with common methods and have the child objects inherit them.


Attribute Name Mangling

In the introduction we saw that that the underscore naming convention __atr informs Python to “mangle” the attribute name when the object is instantiated. This means that the attribute name will be converted to _ClassName__atr for reference. For example,

class example:
	def regularName(self):
		self.key = 1
	def __mangledName(self):
		self.value = 1
x = example()
print(dir(x))
> [...'_example__mangledName', 'regularName']  # omitted other output

As you can see from the above, the the __mangledName was converted to _example__mangledName. Typically this form of name mangling is done to classify an object attribute as private. It is also used to prevent naming collisions between instances of classes.


Class Objects Example

Now that we’ve learned the basics of Python objects and the class object constructor, lets go back to our motivating example of the geography class. We want to create a way to store student attributes and perform calculations on this information.

Let’s assume that our data was input as the following list of dictionaries:

geographyClass = [{"Name":"Mary" ,"HW":85 ,"P1":90 ,"M":88 ,"P2":100 ,"F":90},
				  {"Name":"Matthew" ,"HW":80 ,"P1":95 ,"M":70 ,"P2":90 ,"F":70},
				  {"Name":"Marie" ,"HW":90 ,"P1":92 ,"M":84 ,"P2":0 ,"F":91},
				  {"Name":"Manuel" ,"HW":79 ,"P1":70 ,"M":85 ,"P2":70 ,"F":82},
				  {"Name":"Malala" ,"HW":100 ,"P1":95 ,"M":100 ,"P2":98 ,"F":99}]

We can create objects by establishing a geographyStudent class that contains attributes for input from the dictionary:

class geographyStudent:
	def __init__(self, input):  #Initializing each grade attribute
		self.name = input["Name"] 
		self.homework = input["HW"]  
		self.project1 = input["P1"]
		self.midterm = input["M"]
		self.project2 = input["P2"]
		self.final = input["F"]

Now we can add the data to a list of geographyStudent class objects.

students = []  # Define an empty list in which to place objects
for s in geographyClass:
	students.append(geographyStudent(s)) # Instantiate an object for each student
print(students)
> [<__main__.geographyStudent at 0x190feca1d30>,
>  <__main__.geographyStudent at 0x190feca1d68>,
>  <__main__.geographyStudent at 0x190feca1da0>,
>  <__main__.geographyStudent at 0x190feca1dd8>,
>  <__main__.geographyStudent at 0x190feca1e10>]
print(students[0].name)
> Mary

Now that we have defined a geography student class constructor, we can add methods to it to create some summaries of the student’s grade. We can create the finalGrade method to calculate the student’s final grade for the semester:

class geographyStudent:
	def __init__(self, input):  #Initializing each grade attribute
		self.name = input["Name"] 
		self.homework = input["HW"]  
		self.project1 = input["P1"]
		self.midterm = input["M"]
		self.project2 = input["P2"]
		self.final = input["F"]
	def finalGrade(self):
		self.grade = (self.homework*0.2
					  +  self.project1*0.1
					  +  self.midterm*0.2
					  +  self.project2*0.1
					  +  self.final*0.4)
		return(self.grade)
	
students = []
grades = []
for s in geographyClass:
	students.append(geographyStudent(s))
for s in students:
	grades.append(s.finalGrade())
print(grades)
> [89.6, 76.5, 80.4, 79.6, 98.9]
Can't get enough Python?

Enter your email address for more free Python tutorials and tips.

Python is powerful! Show me more free Python tips

Python Objects Practice Problems

  1. Create class object that supports the properties of a Doubly Linked List. The main object should be the list, and each list element should be an object with key, next, and previous attributes. Use None as a null object.     Solution

  2. Suppose every student in the school we’ve mentioned above has an attribute x.school = "Example High". Build a class object that defines this more general student attribute, and recreate the geographyStudent object from above that inherits it.     Solution

  3. List all the methods associated with Boolean, Integer, and Float objects.     Solution

  4. Consider the following Python code:

class example:
    key = "The best of times."
x = example
x.key = "The worst of times."
y  = example()
print(y.key)
> The worst of times.

Why did y, a new example object, have the same key as x despite seemingly only changing the attribute within x?     Solution

  1. Add a method to the geographyStudent class in the above example to print “Pass” if the student received a total grade greater than or equal to 65, or “Fail” otherwise.     Solution

Solutions

1. Below is one such implementation.

class DoublyLLElement:
	def __init__(self):
		self.key = None
		self.next = None
		self.prev = None
		
class DoublyLinkedList:
	def __init__(self):
		self.root = None

DLL = DoublyLinkedList()
x = DoublyLLElement()
DLL.root = x
x.key = "First Element"
print(DLL.root.key)
> 'First Element'

2. This example motivates the use of inheritance:

class Student:
	school = "Example High"
class geographyStudent(Student):  # This is where inheritance is invoked
	def __init__(self, input):  #Initializing each grade attribute
		self.name = input["Name"] 
		self.homework = input["HW"]  
		self.project1 = input["P1"]
		self.midterm = input["M"]
		self.project2 = input["P2"]
		self.final = input["F"]
	def finalGrade(self):
		self.grade = (self.homework*0.2
					  +  self.project1*0.1
					  +  self.midterm*0.2
					  +  self.project2*0.1
					  +  self.final*0.4)
		return(self.grade)
students = [] 
for s in geographyClass:  # Assumes the list of dictionaries in the above example exists
	students.append(geographyStudent(s))
print(students[1].school)
> Example High

3. The lists themselves are too long to print here, but each one can be printed with:

print(dir(True))  # Boolean
print(dir(1))  # Integer
print(dir(1.0))  # Float

4. The reason is that the x variable erroneously pointed to the class constructor, rather than an object created with the constructor. Because x points to the constructor, the attribute key in the constructor was changed, and the following instantiations of the class object contain the changed key value by default. It’s important to remember the difference between class objects and class object constructors and how objects are instantiated.

5. We can simply add another method that utilizes the existing grade method:

class geographyStudent(Student):
	def __init__(self, input):  
		self.name = input["Name"] 
		self.homework = input["HW"]  
		self.project1 = input["P1"]
		self.midterm = input["M"]
		self.project2 = input["P2"]
		self.final = input["F"]
	def finalGrade(self):
		self.grade = (self.homework*0.2
					  +  self.project1*0.1
					  +  self.midterm*0.2
					  +  self.project2*0.1
					  +  self.final*0.4)
		return(self.grade)
	def PassFail(self):
		grade = self.finalGrade()
		if grade >= 65.0:
			return("Pass")
		else:
			return("Fail")
students = [] 
for s in geographyClass:  # Assumes the list of dictionaries in the above example exists
	students.append(geographyStudent(s))
print(students[1].finalGrade())
> 76.5
print(students[1].PassFail())
> Pass

Did you find this free tutorial helpful? Share this article with your friends, classmates, and coworkers on Facebook and Twitter! When you spread the word on social media, you’re helping us grow so we can continue to provide free tutorials like this one for years to come.

  1. The dir function actually accesses an object’s __dir__ method to obtain the necessary information. If the __dir__ method is not defined within the object, then Python will attempt to gather what information it can. Note that dir(x) is equivalent to x.__dir__()

  2. “private” is used in quotes here because the attribute can still be accessed externally by outside user via object._atr. The “private” status means that Python will not automatically import the attribute, and stands as a caution sign to outside users rather than a locked door. 

  3. The first object doesn’t have to be titled self; it can be any variable name consistent within the class object as long as it’s the first object in the input tuple. However, Python convention uses “self” as the object self-reference, and deviating from convention can cause confusion to later reviewers of the code.