Redefining Special Methods

PythonPythonIntermediate
Practice Now

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

Introduction

In this lab, you will learn how to customize the behavior of objects by redefining special methods. You'll also change the way user - defined objects are printed and make objects comparable.

Additionally, you will learn to create a context manager. The file to be modified in this lab is stock.py.

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 intermediate level lab with a 74% completion rate. It has received a 92% positive review rate from learners.

Improving Object Representation with __repr__

In Python, objects can be represented as strings in two different ways. These representations serve different purposes and are useful in various scenarios.

The first type is the string representation. This is created by the str() function, which is automatically called when you use the print() function. The string representation is designed to be human-readable. It presents the object in a format that is easy for us to understand and interpret.

The second type is the code representation. This is generated by the repr() function. The code representation shows the code that you would need to write to recreate the object. It's more about providing a precise and unambiguous way to represent the object in code.

Let's look at a concrete example using Python's built-in date class. This will help you see the difference between the string and code representations in action.

>>> from datetime import date
>>> d = date(2008, 7, 5)
>>> print(d)              ## Uses str()
2008-07-05
>>> d                     ## Uses repr()
datetime.date(2008, 7, 5)

In this example, when we use print(d), Python calls the str() function on the date object d, and we get a human-readable date in the format YYYY-MM-DD. When we simply type d in the interactive shell, Python calls the repr() function, and we see the code needed to recreate the date object.

You can explicitly get the repr() string in various ways. Here are some examples:

>>> print('The date is', repr(d))
The date is datetime.date(2008, 7, 5)
>>> print(f'The date is {d!r}')
The date is datetime.date(2008, 7, 5)
>>> print('The date is %r' % d)
The date is datetime.date(2008, 7, 5)

Now, let's apply this concept to our Stock class. We're going to improve the class by implementing the __repr__ method. This special method is called by Python when it needs the code representation of an object.

To do this, open the file stock.py in your editor. Then, add the __repr__ method to the Stock class. The __repr__ method should return a string that shows the code needed to recreate the Stock object.

def __repr__(self):
    return f"Stock('{self.name}', {self.shares}, {self.price})"

After adding the __repr__ method, your complete Stock class should now look like this:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, shares):
        self.shares -= shares

    def __repr__(self):
        return f"Stock('{self.name}', {self.shares}, {self.price})"

Now, let's test our modified Stock class. Open a Python interactive shell by running the following command in your terminal:

python3

Once the interactive shell is open, try the following commands:

>>> import stock
>>> goog = stock.Stock('GOOG', 100, 490.10)
>>> goog
Stock('GOOG', 100, 490.1)

You can also see how the __repr__ method works with a portfolio of stocks. Here's an example:

>>> import reader
>>> portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
>>> portfolio
[Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1), Stock('CAT', 150, 83.44), Stock('MSFT', 200, 51.23), Stock('GE', 95, 40.37), Stock('MSFT', 50, 65.1), Stock('IBM', 100, 70.44)]

As you can see, the __repr__ method has made our Stock objects much more informative when displayed in the interactive shell or debugger. It now shows the code needed to recreate each object, which is very useful for debugging and understanding the state of the objects.

When you're done testing, you can exit the Python interpreter by running the following command:

>>> exit()
✨ Check Solution and Practice

Making Objects Comparable with __eq__

In Python, when you use the == operator to compare two objects, Python actually calls the __eq__ special method. By default, this method compares the identities of objects, which means it checks if they are stored at the same memory address, rather than comparing their contents.

Let's take a look at an example. Suppose we have a Stock class, and we create two Stock objects with the same values. Then we try to compare them using the == operator. Here's how you can do it in the Python interpreter:

First, start the Python interpreter by running the following command in your terminal:

python3

Then, in the Python interpreter, execute the following code:

>>> import stock
>>> a = stock.Stock('GOOG', 100, 490.1)
>>> b = stock.Stock('GOOG', 100, 490.1)
>>> a == b
False

As you can see, even though the two Stock objects a and b have the same values for their attributes (name, shares, and price), Python considers them different objects because they are stored at different memory locations.

To fix this issue, we can implement the __eq__ method in our Stock class. This method will be called every time the == operator is used on objects of the Stock class.

Now, open the stock.py file again. Inside the Stock class, add the following __eq__ method:

def __eq__(self, other):
    return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
                                         (other.name, other.shares, other.price))

Let's break down what this method does:

  1. First, it uses the isinstance function to check if the other object is an instance of the Stock class. This is important because we only want to compare Stock objects with other Stock objects.
  2. If other is a Stock object, it then compares the attributes (name, shares, and price) of both the self object and the other object.
  3. It returns True only if both objects are Stock instances and their attributes are identical.

After adding the __eq__ method, your complete Stock class should look like this:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, shares):
        self.shares -= shares

    def __repr__(self):
        return f"Stock('{self.name}', {self.shares}, {self.price})"

    def __eq__(self, other):
        return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
                                             (other.name, other.shares, other.price))

Now, let's test our improved Stock class. Start the Python interpreter again:

python3

Then, run the following code in the Python interpreter:

>>> import stock
>>> a = stock.Stock('GOOG', 100, 490.1)
>>> b = stock.Stock('GOOG', 100, 490.1)
>>> a == b
True
>>> c = stock.Stock('GOOG', 200, 490.1)
>>> a == c
False

Great! Now our Stock objects can be properly compared based on their content, rather than their memory addresses.

The isinstance check in the __eq__ method is crucial. It ensures that we are only comparing Stock objects. If we didn't have this check, comparing a Stock object with something that is not a Stock object might raise errors.

When you're done testing, you can exit the Python interpreter by running the following command:

>>> exit()
✨ Check Solution and Practice

Creating a Context Manager

A context manager is a special type of object in Python. In Python, objects can have different methods that define their behavior. A context manager specifically defines two important methods: __enter__ and __exit__. These methods work together with the with statement. The with statement is used to set up a specific context for a block of code. Think of it as creating a little environment where certain things happen, and when the block of code is finished, the context manager takes care of cleaning up.

In this step, we're going to create a context manager that has a very useful function. It will temporarily redirect the standard output (sys.stdout). Standard output is where the normal output of your Python program goes, usually the console. By redirecting it, we can send the output to a file instead. This is handy when you want to save the output that would otherwise just be displayed on the console.

First, we need to create a new file to write our context manager code. We'll name this file redirect.py. You can create it using the following command in the terminal:

touch /home/labex/project/redirect.py

Now that the file is created, open it in an editor. Once it's open, add the following Python code to the file:

import sys

class redirect_stdout:
    def __init__(self, out_file):
        self.out_file = out_file

    def __enter__(self):
        self.stdout = sys.stdout
        sys.stdout = self.out_file
        return self.out_file

    def __exit__(self, ty, val, tb):
        sys.stdout = self.stdout

Let's break down what this context manager does:

  1. __init__: This is the initialization method. When we create an instance of the redirect_stdout class, we pass in a file object. This method stores that file object in the instance variable self.out_file. So, it remembers where we want to redirect the output to.
  2. __enter__:
    • First, it saves the current sys.stdout. This is important because we need to restore it later.
    • Then, it replaces the current sys.stdout with our file object. From this point on, any output that would normally go to the console will go to the file instead.
    • Finally, it returns the file object. This is useful because we might want to use the file object inside the with block.
  3. __exit__:
    • This method restores the original sys.stdout. So, after the with block is finished, the output will go back to the console as normal.
    • It takes three parameters: exception type (ty), exception value (val), and traceback (tb). These parameters are required by the context manager protocol. They are used to handle any exceptions that might occur inside the with block.

Now, let's test our context manager. We'll use it to redirect the output of a table to a file. First, start the Python interpreter:

python3

Then, run the following Python code in the interpreter:

>>> import stock, reader, tableformat
>>> from redirect import redirect_stdout
>>> portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
>>> formatter = tableformat.create_formatter('text')
>>> with redirect_stdout(open('out.txt', 'w')) as file:
...     tableformat.print_table(portfolio, ['name','shares','price'], formatter)
...     file.close()
...
>>> ## Let's check the content of the output file
>>> print(open('out.txt').read())
      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

Great! Our context manager worked as expected. It successfully redirected the table output to the file out.txt.

Context managers are a very powerful feature in Python. They help you manage resources properly. Here are some common use cases for context managers:

  • File operations: When you open a file, a context manager can make sure the file is closed properly, even if an error occurs.
  • Database connections: It can ensure that the database connection is closed after you're done using it.
  • Locks in threaded programs: Context managers can handle locking and unlocking resources in a safe way.
  • Temporarily changing environment settings: You can change some settings for a block of code and then restore them automatically.

This pattern is very important because it ensures that resources are properly cleaned up, even if an exception occurs inside the with block.

When you're done testing, you can exit the Python interpreter:

>>> exit()

Summary

In this lab, you have learned how to customize the string representation of objects using the __repr__ method, make objects comparable with the __eq__ method, and create a context manager using the __enter__ and __exit__ methods. These special "dunder methods" are the cornerstone of Python's object - oriented features.

Implementing these methods in your classes allows your objects to behave like built - in types and integrate smoothly with Python's language features. Special methods enable various functionalities such as custom string representations, object comparison, and context management. As you progress in Python, you'll uncover more special methods to leverage its powerful object model.