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"])

🧠 Check Your Understanding

Which of these are class variables?

Which of these are instance variables?

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:

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.

➡️ Next up: Exercise: More on classes