Classes
Let's imagine that we're building an online chocolate shop, and we want to use object-oriented programming to organize our code for the shop. (This is not such a far-fetched fantasy; my friend is the CTO of a chocolate shop that's written in Python).
Before writing code, it helps to think about what sort of data and functionality our shop is dealing with.
In terms of data, we need to keep track of what kind of chocolate bars we're selling, their name and price, and how much inventory remains of each bar. If inventory runs low, we can mark them as "sold out" on the website and make sure to ramp up production on that type of bar. We are of course very much hoping our shop will attract customers, and each customer has a name and address.
In terms of functionality, customers need to be able to order a type of chocolate bar, perhaps ordering more than 1 bar, and specify how they will pay for their bars. When a customer makes an order, the inventory of that bar needs to be adjusted accordingly. Customers will be quite mad if they order bars that no longer exist. 😡
Here's all that in diagram form:
Now let's translate that into object types and methods.
A
Product
represents a kind of chocolate bar, and has a name, price, nutrition information, and inventory amount. It also has methods likeincrease_inventory(amount)
andreduce_inventory(amount)
. Note that I'm choosing forProduct
to represent a kind of bar (like all the "Piña Chocolatta" bars), not an individual bar, since it doesn't seem necessary to track every bar separately.A
Customer
has a name and address, with a method likebuy(product, quantity, cc_info)
.An
Order
has an associated customer, product, quantity, and credit card information. Its associated methods might beship()
andrefund(reason)
.
To make each data type, we need to create a class. A class is a template for describing new data types, and is responsible for managing the data associated with an object (such as the name and address of a customer) and defining the associated methods (like buying a product).
Class initialization
To define a new class (a new object type), we first write the class
keyword followed by the class name and a colon.
class Product:
By convention, class names for new classes always start with a capital letter, even though many of the built-in class names are lowercase (e.g.float
, int
, str
).
We will put all the associated data and methods after the colon, indented inside the class definition.
Most classes start off with a special method named __init__
. This method is called by Python whenever someone wants to initialize a new object of that type, and is responsible for storing the initial values of the object's associated data.
For a product, we would like to initialize it with a name, price, and nutrition info (that we pass in) plus a starting inventory of 0:
class Product:
def __init__(self, name, price, nutrition_info):
self.name = name
self.price = price
self.nutrition_info = nutrition_info
self.inventory = 0
This is the line of code that constructs and initializes a new object:
pina_bar = Product("Piña Chocolotta", 7.99, ["200 calories", "24 g sugar"])
When that line of code is called, Python creates a new instance of the class (a new object). It then calls the __init__
method and passes in the new object as the first argument (named self
), along with the additional three arguments.
The method uses dot notation to initialize data for the current object (self
) according to the arguments:
self.name = name
self.price = price
self.nutrition_info = nutrition_info
self.inventory = 0
Instance variables
An instance variable is a data attribute that describes the state of an object.
An initialized Product
object has four instance variables: name
, price
, nutrition_info
, inventory
. You can't just set instance variables by passing them into __init__
, you have to actually set them on the object using dot notation.
For example, the following initialization method only sets a single instance variable:
def __init__(self, name, price, nutrition_info):
self.inventory = 0
Relatedly, the name of the instance variable depends on the name that comes after the dot, not the name of the passed in argument.
This initialization method does assign the same four instance variable names, despite using horribly short argument names:
def __init__(self, n, p, ni):
self.name = n
self.price = p
self.nutrition_info = ni
self.inventory = 0
It's common to use the same argument names as attribute names, but keep this distinction in mind since it may help you with debugging down the road.
The instance variables can also be referenced using dot notation outside of the class, like so:
pina_bar = Product("Piña Chocolotta", 7.99, ["200 calories", "24 g sugar"])
print(pina_bar.name)
print(pina_bar.price)
print(f"A {pina_bar.name} bar costs {pina_bar.price}. Only {pina_bar.inventory} left!")
Method definition
The next step is to define custom methods for the class. A method is a function that belongs to a class, so a method definition is just a function definition inside the class definition.
We typically define custom methods after the __init__
method, like so:
class Product:
# define __init__ first
def increase_inventory(self, amount):
self.inventory += amount
Then we can call that method using dot notation on any instance of that class:
pina_bar = Product("Piña Chocolotta", 7.99, ["200 calories", "24 g sugar"])
pina_bar.increase_inventory(2)
Once again, notice that self
is first in the parameter list, but is not explicitly passed into the method. When you call a method on an object, Python finds the function definition inside the object's class definition and calls that function with the object as self
.
In fact, if you really wanted, you could bypass the Python method magic, treat the method as a standard function, and pass self
in directly, like this:
Product.increase_inventory(pina_bar, 2)
But it's shorter and a whole lot more convenient to simply call the method on the object itself.
Dot notation
As you've now seen, dot notation is used to access all attributes of an object, both the instance variables and the methods.
pina_bar.name
pina_bar.increase_inventory(2)
The variable name on the left side of the dot notation can actually be any expression that evaluates to an object reference.
This code makes a list of objects, then uses bracket notation to grab a single object and dot notation after to call the object's method:
bars = [pina_bar, truffle_bar]
bars[0].increase_inventory(2)
The full class definition
Let's put it all together now!
The definition for a class with four instance variables and two custom methods:
# Define a new type of data
class Product:
# Set the initial values
def __init__(self, name, price, nutrition_info):
self.name = name
self.price = price
self.nutrition_info = nutrition_info
self.inventory = 0
# Define methods
def increase_inventory(self, amount):
self.inventory += amount
def reduce_inventory(self, amount):
self.inventory -= amount
Then we initialize multiple instances of the class and call methods on each of the initialized objects:
pina_bar = Product("Piña Chocolotta", 7.99, ["200 calories", "24 g sugar"])
truffle_bar = Product("Trufflapagus", 9.99, ["170 calories", "9 g sugar"])
pina_bar.increase_inventory(2)
truffle_bar.increase_inventory(200)
The great thing about object-oriented programming is that we can initialize hundreds of products, and know that each of those products have the same variable names and methods. A little bit of code can go a long way!