How Objects Are Represented

Beginner

This tutorial is from open-source community. Access the source code

Introduction

In this lab, you will learn how Python objects are represented internally and understand the mechanisms of attribute assignment and lookup. These concepts are fundamental to grasping how Python manages data and behavior within objects.

Additionally, you will explore the relationship between classes and instances and examine the role of class definitions in object-oriented programming. This knowledge will enhance your understanding of Python's object-oriented features.

This is a Guided Lab, which provides step-by-step instructions to help you learn and practice. Follow the instructions carefully to complete each step and gain hands-on experience. Historical data shows that this is a beginner level lab with a 94% completion rate. It has received a 93% positive review rate from learners.

Creating a Simple Stock Class

In this step, we're going to create a simple class to represent a stock. Understanding how to create classes is fundamental in Python, as it allows us to model real - world objects and their behaviors. This simple stock class will be our starting point to explore how Python objects work internally.

To begin, we need to open a Python interactive shell. The Python interactive shell is a great place to experiment with Python code. You can type and execute Python commands one by one. To open it, type the following command in the terminal:

python3

Once you've entered the command, you'll see the Python prompt (>>>). This indicates that you're now inside the Python interactive shell and can start writing Python code.

Now, let's define a SimpleStock class. A class in Python is like a blueprint for creating objects. It defines the attributes (data) and methods (functions) that the objects of that class will have. Here's how you define the SimpleStock class with the necessary attributes and methods:

>>> class SimpleStock:
...     def __init__(self, name, shares, price):
...         self.name = name
...         self.shares = shares
...         self.price = price
...     def cost(self):
...         return self.shares * self.price
...

In the code above, the __init__ method is a special method in Python classes. It's called a constructor, and it's used to initialize the object's attributes when an object is created. The self parameter refers to the instance of the class that's being created. The cost method calculates the total cost of the shares by multiplying the number of shares by the price per share.

After defining the class, we can create instances of the SimpleStock class. An instance is an actual object created from the class blueprint. Let's create two instances to represent different stocks:

>>> goog = SimpleStock('GOOG', 100, 490.10)
>>> ibm = SimpleStock('IBM', 50, 91.23)

These instances represent 100 shares of Google stock at $490.10 per share and 50 shares of IBM stock at $91.23 per share. Each instance has its own set of attribute values.

Let's verify that our instances are working properly. We can do this by checking their attributes and calculating their cost. This will help us confirm that the class and its methods are functioning as expected.

>>> goog.name
'GOOG'
>>> goog.shares
100
>>> goog.price
490.1
>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5

The cost() method multiplies the number of shares by the price per share to calculate the total cost of holding those shares. By running these commands, we can see that the instances have the correct attribute values and that the cost method is calculating the cost accurately.

Exploring the Internal Dictionary of Objects

In Python, objects are a fundamental concept. An object can be thought of as a container that holds data and has certain behaviors. One of the interesting aspects of Python objects is how they store their attributes. Attributes are like variables that belong to an object. Python stores these attributes in a special dictionary, which can be accessed through the __dict__ attribute. This dictionary is an internal part of the object, and it's where Python keeps track of all the data associated with that object.

Let's take a closer look at this internal structure using our SimpleStock instances. In Python, an instance is an individual object created from a class. For example, if SimpleStock is a class, goog and ibm are instances of that class.

To see the internal dictionary of these instances, we can use the Python interactive shell. The Python interactive shell is a great tool for quickly testing code and seeing the results. In the Python interactive shell, type the following commands to inspect the __dict__ attribute of our instances:

>>> goog.__dict__
{'name': 'GOOG', 'shares': 100, 'price': 490.1}
>>> ibm.__dict__
{'name': 'IBM', 'shares': 50, 'price': 91.23}

When you run these commands, the output shows that each instance has its own internal dictionary. This dictionary contains all the instance attributes. For example, in the goog instance, the attributes name, shares, and price are stored in the dictionary with their corresponding values. This is how Python implements object attributes behind the scenes. Every object has a private dictionary that holds all of its attributes.

It's important to understand what the __dict__ attribute reveals about the internal implementation of our objects:

  1. The keys in the dictionary correspond to the attribute names. For example, in the goog instance, the key 'name' corresponds to the attribute name of the object.
  2. The values in the dictionary are the attribute values. So, the value 'GOOG' is the value of the name attribute for the goog instance.
  3. Each instance has its own separate __dict__. This means that the attributes of one instance are independent of the attributes of another instance. For example, the shares attribute of the goog instance can be different from the shares attribute of the ibm instance.

This dictionary-based approach allows Python to be very flexible with objects. As we will see in the next step, we can use this flexibility to modify and access object attributes in various ways.

Adding and Modifying Object Attributes

In Python, objects are implemented based on dictionaries. This implementation gives Python a high degree of flexibility when dealing with object attributes. Unlike some other programming languages, Python doesn't limit the attributes of an object to only those defined in its class. This means you can add new attributes to an object at any time, even after the object has been created.

Let's explore this flexibility by adding a new attribute to one of our instances. Suppose we have an instance named goog of a class. We'll add a date attribute to it:

>>> goog.date = "6/11/2007"
>>> goog.__dict__
{'name': 'GOOG', 'shares': 100, 'price': 490.1, 'date': '6/11/2007'}

Here, we added a new attribute date to the goog instance. Notice that this date attribute was not defined in the SimpleStock class. This new attribute exists only on the goog instance. To confirm this, let's check the ibm instance:

>>> ibm.__dict__
{'name': 'IBM', 'shares': 50, 'price': 91.23}
>>> hasattr(ibm, 'date')
False

As we can see, the ibm instance does not have the date attribute. This shows three important characteristics of Python objects:

  1. Each instance has its own unique set of attributes.
  2. Attributes can be added to an object after it has been created.
  3. Adding an attribute to one instance does not affect other instances.

Now, let's try a different way to add an attribute. Instead of using the dot notation, we'll directly manipulate the underlying dictionary of the object. In Python, each object has a special attribute __dict__ which stores all its attributes as key - value pairs.

>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>> goog.__dict__
{'name': 'GOOG', 'shares': 100, 'price': 490.1, 'date': '6/11/2007', 'time': '9:45am'}

By directly modifying the __dict__ dictionary, we added a new attribute time to the goog instance. When we access goog.time, Python looks for the 'time' key in the __dict__ dictionary and returns its corresponding value.

These examples illustrate that Python objects are essentially dictionaries with some extra features. The flexibility of Python objects allows for dynamic modification, which is very powerful and convenient in programming.

Understanding the Relationship Between Classes and Instances

Now, we're going to explore how classes and instances are related in Python, and how method lookup works. This is an important concept because it helps you understand how Python finds and uses methods and attributes when you work with objects.

First, let's check what class our instances belong to. Knowing the class of an instance is crucial as it tells us where Python will look for methods and attributes related to that instance.

>>> goog.__class__
<class '__main__.SimpleStock'>
>>> ibm.__class__
<class '__main__.SimpleStock'>

Both instances have a reference back to the SimpleStock class. This reference is like a pointer that Python uses when it needs to look up methods. When you call a method on an instance, Python uses this reference to find the appropriate method definition.

When you call a method on an instance, Python follows these steps:

  1. It looks for the attribute in the instance's __dict__. The __dict__ of an instance is like a storage area where all the instance-specific attributes are kept.
  2. If not found, it checks the class's __dict__. The class's __dict__ stores all the attributes and methods that are common to all instances of that class.
  3. If found in the class, it returns that attribute.

Let's see this in action. First, verify that the cost method is not in the instance dictionaries. This step helps us understand that the cost method is not specific to each instance but is defined at the class level.

>>> 'cost' in goog.__dict__
False
>>> 'cost' in ibm.__dict__
False

So where is the cost method coming from? Let's check the class. By looking at the class's __dict__, we can find out where the cost method is defined.

>>> SimpleStock.__dict__['cost']
<function SimpleStock.cost at 0x7f...>

The method is defined in the class, not in the instances. When you call goog.cost(), Python doesn't find cost in goog.__dict__, so it looks in SimpleStock.__dict__ and finds it there.

You can actually call the method directly from the class dictionary, passing the instance as the first argument (which becomes self). This shows how Python internally calls methods when you use the normal instance.method() syntax.

>>> SimpleStock.__dict__['cost'](goog)
49010.0
>>> SimpleStock.__dict__['cost'](ibm)
4561.5

This is essentially what Python does behind the scenes when you call goog.cost().

Now, let's add a class attribute. Class attributes are shared by all instances. This means that all instances of the class can access this attribute, and it's stored only once at the class level.

>>> SimpleStock.exchange = 'NASDAQ'
>>> goog.exchange
'NASDAQ'
>>> ibm.exchange
'NASDAQ'

Both instances can access the exchange attribute, but it's not stored in their individual dictionaries. Let's verify this by checking the instance and class dictionaries.

>>> 'exchange' in goog.__dict__
False
>>> 'exchange' in SimpleStock.__dict__
True
>>> SimpleStock.__dict__['exchange']
'NASDAQ'

This demonstrates that:

  1. Class attributes are shared by all instances. All instances can access the same class attribute without having their own copy.
  2. Python checks the instance dictionary first, then the class dictionary. This is the order in which Python looks for attributes when you try to access them on an instance.
  3. Classes act as a repository for shared data and behavior (methods). The class stores all the common attributes and methods that can be used by all its instances.

If we modify an instance attribute with the same name, it shadows the class attribute. This means that when you access the attribute on that instance, Python will use the instance-specific value instead of the class-level value.

>>> ibm.exchange = 'NYSE'
>>> ibm.exchange
'NYSE'
>>> goog.exchange  ## Still using the class attribute
'NASDAQ'
>>> ibm.__dict__['exchange']
'NYSE'

Now ibm has its own exchange attribute that shadows the class attribute, while goog still uses the class attribute.

Summary

In this lab, you have learned about the inner workings of Python's object system and several key concepts. First, Python objects store attributes in a dictionary accessible via the __dict__ attribute, offering flexibility. Second, you grasped how attribute assignment and lookup work, including dynamic attribute addition and the order of attribute checking.

Moreover, you explored the relationship between classes and instances, where classes hold shared data and behavior, and instances maintain their own state. You also discovered how method calls operate, with methods in the class acting on instances through the self parameter. Understanding these concepts deepens your insight into Python's OOP model and is useful for debugging, designing class hierarchies, and learning advanced features.