More on classes
Class variables
A class variable is a data attribute that is shared across all the instances of the class (all the objects of that type).
We declare a class variable by writing an assignment statement inside the class definition, before the methods:
class Product:
Note there is no self
since it belongs to the whole class, not to any one particular object.
However, when accessing the class variable inside a methods, we can use self
:
class Product:
sales_tax = 0.07
def __init__(self, name, price, nutrition_info):
self.name = name
self.price = price
self.nutrition_info = nutrition_info
The self.sales_tax
expression works because dot notation starts by looking for an instance variable by that name, and then if it can't find any, it looks for a class variable of that name instead.
It's also possible to reference the class variable by using the class name in the dot notation, like Product.sales_tax
. Accessing the class variable that way ensures that your code won't accidentally reference an instance variable by the same name. It's a bad idea to use the same name for both class variables and instance variables, however, unless you specifically intend for the instance variable to override the class variable.
Exercise: Attribute types
Consider this class representing the customer for the chocolate shop:
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"])
Class methods
A class method is a method that is called on a class and does not receive an object as the first argument. Instead, it receives the class itself as the first argument.
To define a class method, we must put the line @classmethod
before the function definition, and by convention, name the first argument cls
instead of self
:
class Product:
sales_tax = 0.07
def __init__(self, name, price, nutrition_info):
self.name = name
self.price = price
self.nutrition_info = nutrition_info
We call that method on the class and pass in everything but the first argument:
free_dark = Product.make_free_bars("Super Dark", ["200 cals", "5 g sugar"])
That line of code will create a new instance of the class with the given name, nutrition information, and a price of $0.00. That's because cls
gets set to Product
, so cls(name, 0.00, nutrition_info)
is the same as Product(name, 0.00, nutrition_info)
. It might seem silly to write cls
instead of Product
, but once we learn more features of object-oriented programming, we'll discover that using cls
provides more flexibility than hardcoding Product
.
Class methods are commonly used to create "factory methods": methods whose job is to construct and return a new instance of the class, just like the example above. They're not limited to that, however; they could also be used to create multiple instances of a class, or perhaps do some computation on class variables.
The majority of your methods will likely be instance methods, but @classmethod
is a good tool for your OO toolbox.
Public vs. private
In many languages that support object-oriented programming, there are strict rules about which code can access and modify the attributes of an object. Python is not so strict (or, as I usually say, it's quite loosey-goosey!).
For example, any code that has a reference to an object can modify the instance variables of that object:
pina_bar = Product("Piña Chocolotta", 7.99,
["200 calories", "24 g sugar"])
pina_bar.inventory = -5000
Other languages would error on the line that sets the inventory
with complaints that the attribute can't be set outside of its method definitions.
In fact, we can even assign brand new instance variables:
pina_bar.brand_new_attribute_haha = "instanception"
Now, as responsible programmers, we probably wouldn't purposefully do such a thing. But mistakes happen, and it can be nice when languages provide a way for us to save ourselves from ourselves.
Python's approach is to use a naming convention for attribute names:
If there's no underscore in front of an attribute name, like
inventory
, then that attribute is considered publicly accessible, and can be freely accessed and modified wherever.If there's a single underscore in front, like
_inventory
, then it is considered an attribute that should only be used internally within the class definition. We could still writepina_bar._inventory = 500
, but we'd be defying the convention by doing so, and should be prepared for horrible bugs to befall our code.If there's a double underscore in front, like
__inventory
, then Python will try to make it difficult to access that attribute by giving it a different name when it's used outside of the class definition. It is actually possible to figure out what that name will be and still access that attribute, but I won't tell you here since it's a bad idea to try to access attributes that a programmer really wanted to be fully private.
Here's how we could rewrite the Product
class to make inventory into a fully private attribute (or at least, as private as possible in Python).
class Product:
def __init__(self, name, price, nutrition_info):
self.name = name
self.price = price
self.nutrition_info = nutrition_info
With this change, it's much more difficult for code outside of the class definition to reduce the inventory attribute. If the inventory attribute wasn't protected by the double underscores in front, another programmer might accidentally update inventory in another part of the codebase and not realize they needed to update needs_restocking
as well. This change attempts to ensure that all inventory updates go through the restocking check.
Private instance variables are often used to either add error checking, as in this example, or to hide other implementation details.