A class is a blueprint for an object. An object’s class is what defines its attributes and capabilities. Classes are made up of 3 main parts; attributes, constructors and methods.
Definition Syntax
Syntax overview for class declaration and definition:
class <class_name>:
<attribute_name> : <attribute_type>
<attribute_name> : <attribute_type> = <attribute_default_value>
def __init__(self, <parameter_name> : <parameter_type>):
self.<attribute_name> = <parameter_name>
def <method_name>(self, <parameters...>) -> <return_type>:
<method_body>
Example of class declaration:
class Animal:
str
sound:
def __init__(self, noise: str):
self.sound = noise
def make_noise(self, times: int) -> None:
for _ in range(times):
print(self.sound)
Creation Syntax
Initializing a composite data type requires constructing a new object. The general syntax for this is as follows:
<variable_name> : <class_name> = <class_name>(<constructor parameters>)
Example of object creation:
= Animal("oink")
pig: Animal = Animal("woof") dog: Animal
attributes
Classes have attributes. Each object made from a class will have all the attributes established in that class. Additionally, each attribute in a class has a specific type. So, all objects we create from a certain class will contain all attributes defined in the class, and we can customize the values within these attributes for each individual object. For example:
class TarHeel:
str = ""
name: int = 0
PID: bool = true hatesDook:
The attributes declared inside the body of the TarHeel class are name, PID, and hatesDook. Every TarHeel object we make will have these attributes.
In this example, there are defualt values for each attribute of "", 0, true respectively. We can declare two new Tar Heels with all the default attributes as seen below:
= TarHeel()
student1: TarHeel = TarHeel() student2: TarHeel
attribute Access
To access an object’s attribute, just use the name of the object followed by the dot operator (.) followed by the attribute name.
print(student1.PID)
if student2.hatesDook:
print("Go Heels!")
For instance, the code above accesses first the PID attribute of student1 and then the hatesDook attribute of student2. We would expect this to print:
0
! Go Heels
attribute Assignment
We can also use the dot operator (.) in order to reassign attributes of objects. Notice we have to specify which student we are referring to.
= TarHeel()
student1: TarHeel = "Claire"
student1.name = "123456789"
student1.PID
= TarHeel()
student1: TarHeel = "Aneka"
student1.name = "987654321" student1.PID
Now if we were to run the following:
print(student1.name)
print(student2.PID)
It would output:
Claire987654321
Constructors
Every class has a constructor. A class constructor specifies initial attributes of the class from outside of the class to create an instance of the class (also called an object of the class).
A constructor is a “magic” method because it is automagically called when the class is called.
In Python, methods of classes look very similar to functions, but they are defined inside of the class. A constructor is a method that every class has, and it requires specific syntax.
Example
Let’s stick with the TarHeel example:
class TarHeel:
str
name: int
PID: bool = true
hatesDook:
def __init__ (self, student_name: str, student_PID: int):
self.name = student_name
self.PID = student_PID
The constructor always begins with def __init__ (self, parameters)
and has no return type. The body of the constructor reassigns the class’s attributes to the values passed by the parameters. Let’s look at how this works by construction two new TarHeel objects:
= TarHeel("Tori", 500500500)
student3: TarHeel = TarHeel("Ezri", 111222333) student4: TarHeel
We can still access an object’s attribute in the same way as before:
print(student3.name)
if student4.hatesDook:
print("Go Heels!")
This will output:
Tori111222333
We can still change attributes – now, student3’s name is “Baller”
= "Baller" student3.name
But we cannot call an empty constructor.
= TarHeel() # Will FAIL! student5: TarHeel
Choosing what goes into the constructor
Notice that in the TarHeel constructor, we specifically initialized name
and PID
, but not hatesDook
. Because all TarHeels hate dook, hatesDook
can have a default value of True
that does not need to be specified upon construction!
On the other hand, we know each TarHeel has a unique name and PID, so we want to specify these values in the class constructor.
Self
What is up with this self
keyword? Essentially, self
is assigned to a reference of the object you are working with on the heap. If you call a method on student3
, for example, this
will refer to the student3
object on the heap.
In the constructor, self.name
references the name
attribute of the self
object. If we were constructing a new TarHeel object called student6
, this would be student6
’s name
attribute.
Methods
Class methods are specific funtionality written for class instances within the classes themselves.
Syntax
Besides the first parameter of a method being self
, a method’s syntax is the same as a function’s! Method syntax is as follows:
def method_name(self, [params...]) -> return type:
<method body>
Calling a method
Once defined, a method can be called on any object of the class! To call a method, you use the dot operator, similar to how you access attributes of an object. Dissimilar, however, you end this call with double parentheses and any necessary arguments besides self
!
General syntax is: ~~~ {.python} object.method_name(arguments) ~~~
Upon a method call, remember that self
is automatically assigned to the object the method is being called on. While the method evaluates, self references the object on the heap!
Example
Let’s make a new class called Coffee:
class Coffee:
"""A coffee implementation."""
str
customer_name: str
size: bool
whip: int
espresso_shots:
def __init__(self, name: str, size: str):
self.customer_name = name
self.size = size
self.whip = True # assume all people want whip :)
if self.size == "small":
self.espresso_shots = 1
elif self.size == "large":
self.espresso_shots = 2
def make_decaf(self) -> None:
"""Makes the coffee decaf by taking away the espresso."""
self.espresso_shots = 0
def call_customer(self, message: int) -> str:
"""Generates a call for the customer to grab their coffee!"""
str
personal_message:
if message == 1:
= "Have a great day!"
personal_message else:
= "Come back soon!"
personal_message
return f"Your coffee is ready, {self.name}! {personal_message}"
def remove_whip(self) -> None:
"""Removes whip from drink."""
= False whip
In this class, we have 4 attributes, a constructor, and 3 methods! The methods make_decaf
and remove_whip
mutate attributes of the object they’re called upon, and the method call_customer
returns a string unique to the object.
Let’s create some Coffee
objects and call its methods to see what happens:
# creates a small coffee for Aneka with 1 cream, 1 sugar
= Coffee("Aneka", small, 1, 1)
coffee1: Coffee
# creates a large coffee for Tori with 2 cream, 0 sugar
= Coffee("Tori", large, 2, 0)
coffee2: Coffee
# makes Aneka's coffee decaf (have 0 espresso shots)
coffee1.make_decaf()
# removes whip from Tori's coffee
coffee2.remove_whip()
# Tori changes mind, adds back whip to Tori's coffee using direct attribute access
= True
coffee2.whip
# generates and prints messages for the barista
print(coffee1.call_customer(1))
print(coffee2.call_customer(2))
These last two lines will output:
"Your coffee is ready, Aneka! Have a great day!"
"Your coffee is ready, Tori! Come back soon!"
Optional Parameters
Imagine you want to modify your Coffee class. You notice 80% of your customers order large coffees, so you want to simply your implementation to make each Coffee default to large. How could you do this?
The solution to your problem is making your parameters optional by assigning default values to specific parameters. Let’s look at our Coffee constructor.
class Coffee
# ...
def __init__(self, name: str, size: str):
# ...
If we want our coffee to default to large, we want to assign size
a default value of large
. A Coffee object can be specified to be small upon construction, but the size may also be left out to default the coffee size to large. Check out the init parameters:
class Coffee
# ...
def __init__(self, name: str, size: str = "large"):
# ...
Let’s see how construction works now:
# creates a large coffee for Ezri
= Coffee("Ezri")
coffee_3: Coffee
# creates a small coffee for Claire
= Coffee("Claire", small) coffee_4: Coffee
Notice that we left out the size of Ezri’s coffee upon construction… Since this argument is missing, the size variable is assigned to the default “large”. Claire’s coffee size is specified, so the default “large” is ignored.