Overview

Welcome back to our Python programming tutorial. In this article, we will discuss the important concepts of instance variables and class attributes:

  • Instance variables are variables that are unique to each instance of a class.
  • Class attributes are variables that are shared by all instances of a class.

We will use the Coffee class to illustrate the difference between instance variables and class attributes. Unlike our previous article that discussed class, object and constructor, this time, our Coffee class will be associated with the price of the coffee instead of calories.

class Coffee:

    name = "Coffee"
    price = 1.00

    def __init__(self, name, price):
        self.n = name
        self.p = price

The name and price variables are class attributes. They are shared by all instances of the Coffee class.

The self.n and self.p variables are instance variables. They are unique to each instance of the Coffee class.

For example, the following code creates two instances of the Coffee class:

drink1 = Coffee("Latte", 2.00)
drink2 = Coffee("Espresso", 1.50)

The drink1 instance has a name of "Latte" and a price of 2.00. The drink2 instance has a name of "Espresso" and a price of 1.50.

The name and price class attributes are shared by both instances. However, the self.n and self.p instance variables are unique to each instance.

Concept of Object Oriented Programming

Class attributes and instance variables are important concepts in Python programming. By understanding the difference between these two types of variables, you can write more efficient and effective code. Now, let’s explore them in detail.

Exploring Instance Variables Using __dict__

In this code snippet, we embark on an exploration of instance variables in Python. Instance variables are attributes that are specific to individual instances of a class. They hold unique data for each object and contribute to the object's state.

class Coffee:
    def __init__(self,name):
        self.n = name
    def showInfo(self):
        print("Name: {}".format(self.n))

drink1 = Coffee("Latte")
drink1.price = 2.00
drink1.showInfo()

print(drink1.__dict__)

In this code:

  • We define a Coffee class with a constructor __init__ that takes the argument name and initializes an instance variable n with the provided name.
  • The showInfo method prints the value of the instance variable n.
  • An instance of the Coffee class, drink1, is created with the name "Latte".
  • A new instance variable price is added to the drink1 object and assigned the value 2.00.
  • The showInfo method is called on the drink1 object to display its name.
  • Finally, we use the __dict__ attribute to print the dictionary of instance variables associated with the drink1 object.
OUTPUT:

This example illustrates how instance variables allow us to attach unique data to individual objects, enhancing the flexibility and customization of our classes.

Managing Data with Instance Variables in Python

In this code snippet, we'll explore how instance variables work in Python and how they enable us to manage data specific to individual instances of a class. Take a look at the following code.

class Coffee:
    def __init__(self,name):
        self.n = name
    def showInfo(self):
        print("Name: {}".format(self.n))

drink1 = Coffee("Latte")
drink1.price = 2.00
drink1.showInfo()

drink2 = Coffee("Americano")
drink2.price = 1.50
drink2.showInfo()

print(drink1.__dict__)
print(drink2.__dict__)

#modify price for object drink1
drink1.price = 2.75
print("Modify the price for object drink1:")
print(drink1.__dict__)
print(drink2.__dict__)

Let's break down the provided code step by step:

  1. Class Definition: We start by defining a class named Coffee. The class has a constructor (__init__) method that takes a parameter name. Inside the constructor, an instance variable self.n is created and assigned the value of the name parameter. This instance variable stores the name of the coffee drink.
  2. Creating Instances: We then proceed to create two instances of the Coffee class: drink1 and drink2. During instance creation, the __init__ method is automatically called, and the instance variables are initialized with the respective name values.
  3. Adding Instance Variables Dynamically: Uniquely, in Python, you can dynamically add new instance variables to an object outside of the class definition. In this example, we add an instance variable price to drink1 and an instance variable price to drink2.
  4. Accessing Instance Variables: Both instances have a method showInfo that prints out the instance's name. By calling the method on each instance, we get their respective names printed.
  5. Displaying Instance Variables: We use the __dict__ attribute to display the dictionary of instance variables for each object. This provides insights into the instance's attribute-value pairs.
  6. Modifying Instance Variables: We modify the price instance variable for drink1 by assigning a new value. This demonstrates how instance variables can be easily modified after creation. After this modification, we print the __dict__ of both instances to observe the change.

Mangling

Mangling is the process of converting a name with two or more leading underscores (__) to a unique name that is prefixed with the name of the class and a single underscore. This is done to prevent name collisions between attributes in a class and its subclasses.

class Coffee:
    def __init__(self, name):
        self.n = name
        self.__price = 1.00

drink = Coffee("Latte")

# The following line will cause an error because the attribute
# `__ingredient` is mangled to `_Coffee__price`
# and is not accessible from outside the class.

print(drink.__price)

The error message is:

OUTPUT:

This error is caused because the attribute __price is mangled to _Coffee__price when the Coffee class is created. This means that the attribute is not accessible from outside the class.

To access the __price attribute, we need to use the _Coffee__price name. For example, the following code will print the value of the __price attribute:

class Coffee:
    def __init__(self, name):
        self.n = name
        self.__price = 1.00

drink = Coffee("Latte")

print(drink._Coffee__price)

The output of the code is:

OUTPUT:

As you can see, the drink._Coffee__price attribute can be accessed by using the mangled name.

Mangling Limitation for External Private Variables

After understanding mangling, it's important to know its limits. Mangling won't take effect if you try to add a private instance variable outside the class code. In such cases, the variable will act like any other regular property. Let's dive into this with an example:

class Coffee:
    def __init__(self, name = "Coffee", price=1.0):
        self.n = name
        self.__p = price

drink1 = Coffee("Latte", 2.50)
drink2 = Coffee("Americano", 1.50)
drink3 = Coffee("Cappucino", 2.50)

drink3.__price = 2.75

print(drink1.__dict__)
print(drink2.__dict__)
print(drink3.__dict__)

The behavior of these names is a bit tricky. Let's run the program to see the output:

OUTPUT:

Did you notice those strange names full of underscores? Where did they come from? Well, when Python sees that you want to add an instance variable to an object and you're doing it inside any of the object's methods, it mangles the operation like this:

  • It adds the class name before your variable name.
  • It puts an extra underscore at the beginning.

That's why __p becomes _Coffee__p. This mangled name is now fully accessible from outside the class. You can use code like print(drink1._Coffee__p) and get a valid result without errors or exceptions. As you can see, making a property private has its limits.

Understanding Class and Instance Variable

Class variable is a variable that is shared by all instances of a class. It is defined outside of any method in the class, and it is created when the class is created.

Instance variable is a variable that is unique to each instance of a class. It is defined inside a method in the class, and it is created when the instance is created.

class Coffee:
    # This is a class variable. It's shared by all instances of the Coffee class.
    brand = "Nescafe"

    def __init__(self, name, price):
        # This is an instance variable. It's unique to each instance of the Coffee class.
        self.n = name
        self.p = price

drink1 = Coffee("Latte", 2.50)
drink2 = Coffee("Americano", 1.50)

print(drink1.__dict__, drink1.brand)
print(drink2.__dict__, drink1.brand)

In this example, the brand variable is a class variable. It is shared by all instances of the Coffee class, and it is assigned the value "Nescafe" when the class is created. The name and price variables are instance variables. They are unique to each instance of the Coffee class, and they are assigned values when the instance is created.

For the above example, the output is as follows:

OUTPUT:

Two important conclusions come from the example:

  • class variables aren't shown in an object's __dict__ (this is natural as class variables aren't parts of an object) but you can always try to look into the variable of the same name, but at the class level – we'll show you this very soon;
  • a class variable always presents the same value in all class instances (objects)

Understanding Attribute Mangling in Python

Mangling is a process that changes the names of attributes in a class to prevent name collisions when the class is inherited by another class. When an attribute in a class is mangled, its name is prefixed with two underscores and the name of the class is appended to it. For example, if the Coffee class has an attribute called name, the name of the attribute will be mangled to __Coffee__name when the Coffee class is inherited by another class.

Mangling a class variable's name has the same effects as those you're already familiar with.

Look at the example below. Can you guess its output?

class Coffee:
    # This is a class variable. It is shared by all instances of the Coffee class.
    brand = "Nescafe"

    def __init__(self, name, price):
        # This is an instance variable. It is unique to each instance of the Coffee class.
        self.n = name
        self.p = price

drink1 = Coffee("Latte", 2.50)
drink2 = Coffee("Americano", 1.50)

# Mangle the variable name ->> __name
drink1.__name = "Cappucino"

print(drink1.__dict__, drink1.brand)
print(drink2.__dict__, drink1.brand)

Output:

OUTPUT:

Understanding Class Variables and Instance Creation

Previously, we emphasized that class variables are present even before any class instance (object) is created. Now, let's explore the distinction between two specific __dict__ variables: one belonging to the class itself and the other to its instances. This comparison sheds light on their behaviors and outcomes.

Can you guess its output?

class Coffee:
    n = "Nescafe"

    def __init__(self, name):
        Coffee.n = name

print("class Coffee :\n",Coffee.__dict__)
drink = Coffee("Latte")

print("class Coffee :\n",Coffee.__dict__)
print("object drink :\n",drink.__dict__)

Let's take a closer look at it:

  • We define one class named Coffee;
  • The class defines one class variable named n;
  • The class constructor sets the variable with the parameter's value;
  • Naming the variable is the most important aspect of the example because:
    • If you were to modify the assignment within the constructor, for instance, changing it to self.n = name, this would create an instance variable named n that is distinct from the class-level n.
    • Similarly, altering the assignment to something like n = na would operate on a local variable within the method and not affect the class or instance variables.
  • The first line of the off-class code prints the value of the Coffee.name attribute; note – we use the value before the very first object of the class is instantiated.
  • Run the code in the editor and check its output.

Output:

class Coffee :
  {	'__module__': '__main__',
  'n': 'Nescafe',
  '__init__': <function Coffee.__init__ at 0x0000021B80168430>,
  '__dict__': <attribute '__dict__' of 'Coffee' objects>,
  '__weakref__': <attribute '__weakref__' of 'Coffee' objects>,
  '__doc__': None}
class Coffee :
  {	'__module__': '__main__',
  'n': 'Latte',
  '__init__': <function Coffee.__init__ at 0x0000021B80168430>,
  '__dict__': <attribute '__dict__' of 'Coffee' objects>,
  '__weakref__': <attribute '__weakref__' of 'Coffee' objects>,
  '__doc__': None}
object drink :
  {}


  • As you can see, the class's __dict__ contains much more data than its object's counterpart. Most of them are useless now – the one we want you to check carefully shows the current variable value.
  • Note that the object's __dict__ is empty – the object has no instance variables.

Checking An Attribute's Existence

In Python, when you create an object of a class, you are not creating a copy of the class. You are creating a new instance of the class, and the new instance can have its own set of attributes. This means that two objects of the same class can have different sets of attributes.

For example, let's say we have a class called Coffee. The Coffee class has two attributes: name and price.

class Coffee:
    def __init__(self,name,price):
        self.n = name
        self.p = price

We can create two objects of the Car class:

drink1 = Coffee("Latte", 2.50)
drink2 = Coffee("Americano", 1.50)

The drink1 object has the attributes n="Latte" and p=2.5. The drink2 object has the attributes n="Americano" and p=1.5.

As you can see, the two objects have different sets of attributes. This is because each object is a unique instance of the Coffee class.

To check if an attribute exists for an object, you can use the hasattr() function. The hasattr() function takes two arguments: the object and the name of the attribute. The hasattr() function returns True if the attribute exists for the object and False if it does not exist.

For example, the following code checks if given attribute exists for the drink1 object:

print(hasattr(drink1, "n"))       #True
print(hasattr(drink1, "name"))   #False
print(hasattr(drink1, "Latte"))  #False

print(hasattr(drink1, "p"))       #True
print(hasattr(drink1, "price"))   #False
print(hasattr(drink1, "Americano")) #False

The hasattr() function is a powerful tool that can be used to check if an object has a specific attribute. This can be useful for a variety of tasks, such as validating user input or preventing unauthorized access to data.