Classes & Objects
A dictionary groups related data together under one variable, with values you access by name. A class takes that same idea a step further: it groups data together and bundles in the functions that operate on that data, so both travel together as a single unit.
The Python documentation has a guide on Classes.
Defining a Class
A class is defined with the class keyword, and by convention its name is written in CapWords rather than the snake_case used for variables and functions.
class SpacecraftPart:
def __init__(self, name, mass):
self.name = name
self.mass = mass
tank = SpacecraftPart("proptank", 1200.0)
engine = SpacecraftPart("engine", 450.0)
print(tank.name, tank.mass)
print(engine.name, engine.mass)
print(type(tank))
proptank 1200.0
engine 450.0
<class '__main__.SpacecraftPart'>
The __init__ method runs automatically whenever a new object is created, and its job is to set up that object’s starting data.
Creating an object, called instantiating the class, looks like calling the class by name: SpacecraftPart("proptank", 1200.0).
The first parameter of every method, named self by convention, is the object the method was called on.
Assigning to self.name stores a value on that particular object, and reading tank.name later reads it back.
You never pass self in yourself. Python supplies it automatically from whatever object is to the left of the dot.
Variables stored on an object this way are called attributes, and functions defined inside a class are called methods.
Methods
A method is written like a normal function, indented inside the class, with self as its first parameter.
Through self, a method can read whatever data the object is carrying:
class SpacecraftPart:
def __init__(self, name, mass):
self.name = name
self.mass = mass
def describe(self):
print(f"{self.name}: {self.mass} kg")
def weight_on_earth(self):
return self.mass * 9.81
tank = SpacecraftPart("proptank", 1200.0)
tank.describe()
print(f"{tank.weight_on_earth():.1f} N")
proptank: 1200.0 kg
11772.0 N
tank.describe() and tank.weight_on_earth() both operate on tank without being told which part to use, because self already refers to it.
This is the difference between a class and a plain dictionary: the mass and the code that uses the mass live in the same place.
Methods That Modify the Object
A method can also change the object’s data by assigning to an attribute through self:
class SpacecraftPart:
def __init__(self, name, mass):
self.name = name
self.mass = mass
def burn_fuel(self, amount):
if amount > self.mass:
print("Error: cannot burn more than the part's mass.")
return
self.mass -= amount
tank = SpacecraftPart("proptank", 1200.0)
print(tank.mass)
tank.burn_fuel(300)
print(tank.mass)
tank.burn_fuel(5000) # rejected, mass unchanged
print(tank.mass)
1200.0
900.0
Error: cannot burn more than the part's mass.
900.0
Because burn_fuel is the only intended way to reduce the mass, it can also enforce a rule about what changes are valid.
Here it rejects a burn larger than the part’s own mass and leaves the object unchanged, which is harder to guarantee when the data sits loose in a dictionary that any code can write to.
Each Object Holds Its Own Data
Every object created from a class gets its own independent copy of the attributes assigned in __init__:
class SpacecraftPart:
def __init__(self, name, mass):
self.name = name
self.mass = mass
def burn_fuel(self, amount):
self.mass -= amount
tank1 = SpacecraftPart("proptank", 1200.0)
tank2 = SpacecraftPart("proptank", 800.0)
tank1.burn_fuel(100)
print(tank1.mass)
print(tank2.mass) # unaffected by the change to tank1
1100.0
800.0
tank1 and tank2 were created from the same class and share the same methods, but changing one has no effect on the other.
Example: Rocket Thrust-to-Weight
Question
A rocket lifts off only if its thrust exceeds its weight, which is measured by the thrust-to-weight ratio:
\[TWR = \frac{T}{m g}\]Write a Rocket class that stores a name, thrust, and mass, and can report its thrust-to-weight ratio and whether it lifts off.
Use it to compare a heavy lift vehicle (7,600,000 N thrust, 550,000 kg) against an underpowered test article (40,000 N thrust, 9,000 kg).
Solution
The class stores the three values in __init__, and each method computes what it needs from self.
report calls thrust_to_weight and can_lift_off on the same object, so the calculation is written once and reused.
class Rocket:
def __init__(self, name, thrust, mass):
self.name = name
self.thrust = thrust # N
self.mass = mass # kg
def thrust_to_weight(self):
return self.thrust / (self.mass * 9.81)
def can_lift_off(self):
return self.thrust_to_weight() > 1.0
def report(self):
verdict = "yes" if self.can_lift_off() else "no"
print(f"{self.name}: TWR = {self.thrust_to_weight():.2f}, lifts off: {verdict}")
heavy = Rocket("Heavy Lifter", 7600000, 550000)
underpowered = Rocket("Test Article", 40000, 9000)
heavy.report()
underpowered.report()
Heavy Lifter: TWR = 1.41, lifts off: yes
Test Article: TWR = 0.45, lifts off: no
Adding another vehicle takes one more line, and the logic for evaluating it is already written.
When to Use a Class
A class is worth defining when data and behavior belong together, and especially when you have many things of the same kind. For a single group of labeled values that no code needs to act on, a dictionary is simpler and perfectly appropriate. Once you find yourself writing functions that all take the same dictionary as their first argument, that’s a sign the data and those functions want to be a class.
Reading Questions
- What does a class provide that a dictionary does not?
- When does the
__init__method run? - What does
selfrefer to inside a method, and who supplies its value? - What is the difference between an attribute and a method?
- If two objects are created from the same class, does changing an attribute on one affect the other?
- What naming convention does Python use for class names, and how does it differ from the convention for variables?
- Describe a situation where a dictionary would be a better choice than a class.