We could make data abstractions using functions:
# Inventory tracking
add_product(name, price, nutrition)
get_label(product)
get_nutrition_info(product)
increase_inventory(product, amount)
reduce_inventory(product, amount)
# Customer tracking
signup_customer(name, address)
get_greeting(customer)
get_formatted_address(customer)
# Purchase tracking
order(customer, product, quantity, cc_info)
track(order_number)
refund(order_number, reason)
That codebase would be organized around functions.
We can instead organize around objects:
# Inventory tracking
Product(name, price, nutrition)
Product.get_label()
Product.get_nutrition_info()
Product.increase_inventory(amount)
Product.reduce_inventory(amount)
Product.get_inventory_report()
# Customer tracking
Customer(name, address)
Customer.get_greeting()
Customer.get_formatted_address()
Customer.buy(product, quantity, cc_info)
# Purchase tracking
Order(customer, product, quantity, cc_info)
Order.ship()
Order.refund(reason)
# Shop management
ChocolateShop(name)
ChocolateShop.signup_customer(name, address)
ChocolateShop.add_product(name, price, nutrition)
An object bundles together information and related behavior.
Python includes special syntax to create classes and objects.
Each object is an instance of a class. The class is the object's type.
class <name>:
<suite>
class Product:
<suite>
class Customer:
<suite>
class Order:
<suite>
A class can:
class Product:
# Set the initial values
# Define methods
Let's code a class!
# 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
def get_label(self):
return "Foxolate Shop: " + self._name
def get_inventory_report(self):
if self._inventory == 0:
return "There are no bars!"
return f"There are {self._inventory} bars."
pina_bar = Product("Piña Chocolotta", 7.99,
["200 calories", "24 g sugar"])
pina_bar.increase_inventory(2)
class Product:
def __init__(self, name, price, nutrition_info):
def increase_inventory(self, amount):
def reduce_inventory(self, amount):
def get_label(self):
def get_inventory_report(self):
def
statements create attributes of the class (not names in frames).
pina_bar = Product("Piña Chocolotta", 7.99,
["200 calories", "24 g sugar"])
Product(args)
is often called the constructor.
When the constructor is called:
__init__
method of the class is called with the new object as its first argument (named self
), along with any additional arguments provided in the call expression
class Product:
def __init__(self, name, price, nutrition_info):
self._name = name
self._price = price
self._nutrition_info = nutrition_info
self._inventory = 0
All object attributes (which includes variables and methods) can be accessed with dot notation:
pina_bar.increase_inventory(2)
That evaluates to the value of the attribute looked up by increase_inventory
in the object referenced by pina_bar
.
The left-hand side of the dot notation can also be any expression that evaluates to an object reference:
bars = [pina_bar, truffle_bar]
bars[0].increase_inventory(2)
Instance variables are data attributes that describe the state of an object.
This __init__
initializes 4 instance variables:
class Product:
def __init__(self, name, price, nutrition_info):
self._name = name
self._price = price
self._nutrition_info = nutrition_info
self._inventory = 0
The object's methods can then change the values of those variables or assign new variables.
pina_bar.increase_inventory(2)
class Product:
def increase_inventory(self, amount):
self._inventory += amount
pina_bar.increase_inventory
is a bound method:
a function which has its first parameter pre-bound to a particular value.
In this case, self
is pre-bound to pina_bar
and amount
is set to 2.
It's equivalent to:
Product.increase_inventory(pina_bar, 2)
An object can create a new instance variable whenever it'd like.
class Product:
def reduce_inventory(self, amount):
if (self._inventory - amount) <= 0:
self._needs_restocking = True
self._inventory -= amount
pina_bar = Product("Piña Chocolotta", 7.99,
["200 calories", "24 g sugar"])
pina_bar.reduce_inventory(1)
Now pina_bar
has an updated binding for
_inventory
and a new binding for _needs_restocking
(which was not in __init__
).
A class variable is an assignment inside the class that isn't inside a method body.
class Product:
sales_tax = 0.07
Class variables are "shared" across all instances of a class because they are attributes of the class, not the instance.
class Product:
_sales_tax = 0.07
def get_total_price(self, quantity):
return (self._price * (1 + self._sales_tax)) * quantity
pina_bar = Product("Piña Chocolotta", 7.99,
["200 calories", "24 g sugar"])
truffle_bar = Product("Truffalapagus", 9.99,
["170 calories", "19 g sugar"])
pina_bar._sales_tax
truffle_bar._sales_tax
pina_bar.get_total_price(4)
truffle_bar.get_total_price(4)
As long as you have a reference to an object, you can set or mutate any attributes.
pina_bar = Product("Piña Chocolotta", 7.99,
["200 calories", "24 g sugar"])
pina_bar._inventory
pina_bar._inventory = 5000000
pina_bar._inventory = -5000
You can even assign new instance variables:
pina_bar.brand_new_attribute_haha = "instanception"
To communicate the desired access level of attributes, Python programmers generally use this convention:
__
(double underscore) before very private attribute names_
(single underscore) before semi-private attribute namesThat allows classes to hide implementation details and add additional error checking.
We will discuss __
vs _
next time.
For now, if you see no underscore, HAVE FUN! 🎉 🎉
There can be multiple instances of each class.
pina_bar = Product("Piña Chocolotta", 7.99,
["200 calories", "24 g sugar"])
cust1 = Customer("Coco Lover",
["123 Pining St", "Nibbsville", "OH"])
cust2 = Customer("Nomandy Noms",
["34 Shlurpalot St", "Buttertown", "IN"])
What are the classes here?
Product
, Customer
How many instances of each?
1 Product
, 2 Customer
An object can use instance variables to describe its state. A best practice is to hide the representation of the state and manage it entirely via method calls.
>>> pina_bar = Product("Piña Chocolotta", 7.99,
["200 calories", "24 g sugar"])
>>> pina_bar.get_inventory_report()
"There are NO bars!"
>>> pina_bar.increase_inventory(3)
>>> pina_bar.get_inventory_report()
"There are 3 bars total (worth $23.97 total)."
What's the initial state? 0 bars in inventory
What changes the state? increase_inventory()
by changing the instance variable _inventory
class Customer:
_salutation = "Dear"
def __init__(self, name, address):
self._name = name
self._address = address
def get_greeting(self):
return f"{self._salutation} {self._name},"
def get_formatted_address(self):
return "\n".join(self._address)
cust1 = Customer("Coco Lover",
["123 Pining St", "Nibbsville", "OH"])
What are the class variables? _salutation
What are the instance variables? _name
, _address