Attribute Access and Bound Methods

Beginner

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

Introduction

In this lab, you will learn about attribute access in Python. You'll explore how to use functions like getattr() and setattr() to manipulate object attributes effectively.

Additionally, you will experiment with bound methods. The lab will guide you through these concepts, and you'll create a file named tableformat.py during the process.

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 100% completion rate. It has received a 100% positive review rate from learners.

Understanding Attribute Access in Python

In Python, objects are a fundamental concept. They can store data in attributes, which are like named containers for values. You can think of attributes as variables that belong to an object. There are several ways to access these attributes. The most straightforward and commonly used method is the dot (.) notation. However, Python also offers specific functions that give you more flexibility when working with attributes.

The Dot Notation

Let's start by creating a Stock object and see how we can manipulate its attributes using the dot notation. The dot notation is a simple and intuitive way to access and modify an object's attributes.

First, open a new terminal and start the Python interactive shell. This is where you can write and execute Python code line by line.

## Open a new terminal and run Python interactive shell
python3

## Import the Stock class from the stock module
from stock import Stock

## Create a Stock object
s = Stock('GOOG', 100, 490.1)

## Get an attribute
print(s.name)    ## Output: 'GOOG'

## Set an attribute
s.shares = 50
print(s.shares)  ## Output: 50

## Delete an attribute
del s.shares
## If we try to access s.shares now, we'll get an AttributeError

In the code above, we first import the Stock class from the stock module. Then we create an instance of the Stock class named s. To get the value of the name attribute, we use s.name. To change the value of the shares attribute, we simply assign a new value to s.shares. And if we want to remove an attribute, we use the del keyword followed by the attribute name.

Attribute Access Functions

Python provides four built - in functions that are very useful for attribute manipulation. These functions give you more control when working with attributes, especially when you need to handle them dynamically.

  1. getattr() - This function is used to get the value of an attribute.
  2. setattr() - It allows you to set the value of an attribute.
  3. delattr() - You can use this function to delete an attribute.
  4. hasattr() - This function checks if an attribute exists in an object.

Let's see how to use these functions:

## Create a new Stock object
s = Stock('GOOG', 100, 490.1)

## Get an attribute
print(getattr(s, 'name'))       ## Output: 'GOOG'

## Set an attribute
setattr(s, 'shares', 50)
print(s.shares)                 ## Output: 50

## Check if an attribute exists
print(hasattr(s, 'name'))       ## Output: True
print(hasattr(s, 'symbol'))     ## Output: False

## Delete an attribute
delattr(s, 'shares')
print(hasattr(s, 'shares'))     ## Output: False

These functions are particularly useful when you need to work with attributes dynamically. Instead of using hard - coded attribute names, you can use variable names. For example, if you have a variable that stores the name of an attribute, you can pass that variable to these functions to perform operations on the corresponding attribute. This gives you more flexibility in your code, especially when dealing with different objects and attributes in a more dynamic way.

Using getattr() for Generic Object Processing

The getattr() function is a powerful tool in Python that allows you to access attributes of an object in a dynamic way. This is particularly useful when you want to process objects in a generic manner. Instead of writing code that is specific to a particular object type, you can use getattr() to work with any object that has the required attributes. This flexibility makes your code more reusable and adaptable.

Processing Multiple Attributes

Let's start by learning how to access multiple attributes of an object using the getattr() function. This is a common scenario when you need to extract specific information from an object.

First, open a Python interactive shell if you closed the previous one. You can do this by running the following command in your terminal:

## Open a Python interactive shell if you closed the previous one
python3

Next, we'll import the Stock class and create a Stock object. The Stock class represents a stock with attributes like name, shares, and price.

## Import the Stock class and create a stock object
from stock import Stock
s = Stock('GOOG', 100, 490.1)

Now, we'll define a list of attribute names that we want to access. This list will help us iterate over the attributes and print their values.

## Define a list of attribute names
fields = ['name', 'shares', 'price']

Finally, we'll use a for loop to iterate over the list of attribute names and access each attribute using getattr(). We'll print the attribute name and its value for each iteration.

## Access each attribute using getattr()
for name in fields:
    print(f"{name}: {getattr(s, 'name')}" if name == 'name' else f"{name}: {getattr(s, name)}")

When you run this code, you'll see the following output:

name: GOOG
shares: 100
price: 490.1

This output shows that we were able to access and print the values of multiple attributes of the Stock object using the getattr() function.

Default Values with getattr()

The getattr() function also provides a useful feature: the ability to specify a default value if the attribute you're trying to access doesn't exist. This can prevent your code from raising an AttributeError and make it more robust.

Let's see how this works. First, we'll try to access an attribute that doesn't exist in the Stock object. We'll use getattr() and provide a default value of 'N/A'.

## Try to access an attribute that doesn't exist
print(getattr(s, 'symbol', 'N/A'))  ## Output: 'N/A'

In this case, since the symbol attribute doesn't exist in the Stock object, getattr() returns the default value 'N/A'.

Now, let's compare this with accessing an existing attribute. We'll access the name attribute, which does exist in the Stock object.

## Compare with an existing attribute
print(getattr(s, 'name', 'N/A'))    ## Output: 'GOOG'

Here, getattr() returns the actual value of the name attribute, which is 'GOOG'.

Processing a Collection of Objects

The getattr() function becomes even more powerful when you need to process a collection of objects. Let's see how we can use it to process a portfolio of stocks.

First, we'll import the read_portfolio function from the stock module. This function reads a portfolio of stocks from a CSV file and returns a list of Stock objects.

## Import the portfolio reading function
from stock import read_portfolio

Next, we'll use the read_portfolio function to read the portfolio from a CSV file named portfolio.csv.

## Read the portfolio from CSV file
portfolio = read_portfolio('portfolio.csv')

Finally, we'll use a for loop to iterate over the list of Stock objects in the portfolio. For each stock, we'll use getattr() to access the name and shares attributes and print their values.

## Print the name and shares of each stock
for stock in portfolio:
    print(f"Stock: {getattr(stock, 'name')}, Shares: {getattr(stock, 'shares')}")

This approach makes your code more flexible because you can work with attribute names as strings. These strings can be passed as arguments or stored in data structures, allowing you to easily change the attributes you want to access without modifying the core logic of your code.

Creating a Table Formatter Using Attribute Access

In programming, attribute access is a fundamental concept that allows us to interact with the properties of objects. Now, we're going to put what we've learned about attribute access into practice. We'll create a useful utility: a table formatter. This formatter will take a collection of objects and display them in a tabular format, making the data easier to read and understand.

Creating the tableformat.py Module

First, we need to create a new Python file. This file will hold the code for our table formatter.

To create the file, follow these steps:

  1. In the WebIDE, click on the "File" menu.
  2. From the dropdown, select "New File".
  3. Save the newly created file as tableformat.py in the /home/labex/project/ directory.

Now that we have our file, let's write the code for the print_table() function inside tableformat.py. This function will be responsible for formatting and printing our objects in a table.

def print_table(objects, fields):
    """
    Print a collection of objects as a formatted table.

    Args:
        objects: A sequence of objects
        fields: A list of attribute names
    """
    ## Print the header
    headers = fields
    for header in headers:
        print(f"{header:>10}", end=' ')
    print()

    ## Print the separator line
    for header in headers:
        print("-" * 10, end=' ')
    print()

    ## Print the data
    for obj in objects:
        for field in fields:
            value = getattr(obj, field)
            print(f"{value:>10}", end=' ')
        print()

Let's break down what this function does:

  1. It takes two arguments: a sequence of objects and a list of attribute names. The sequence of objects is the data we want to display, and the list of attribute names tells the function which properties of the objects to show.
  2. It prints a header row. The header row contains the names of the attributes we're interested in.
  3. It prints a separator line. This line helps to visually separate the header from the data.
  4. For each object in the sequence, it prints the value of each specified attribute. It uses the getattr() function to access the attribute value of each object.

Now, let's test our print_table() function to see if it works as expected.

## Open a Python interactive shell
python3

## Import our modules
from stock import read_portfolio
import tableformat

## Read the portfolio data
portfolio = read_portfolio('portfolio.csv')

## Print the portfolio as a table with name, shares, and price columns
tableformat.print_table(portfolio, ['name', 'shares', 'price'])

When you run the above code, you should see the following output:

      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

One of the great things about our print_table() function is its flexibility. We can change the columns that are displayed just by changing the fields list.

## Just show shares and name
tableformat.print_table(portfolio, ['shares', 'name'])

Running this code will give you the following output:

    shares       name
---------- ----------
       100         AA
        50        IBM
       150        CAT
       200       MSFT
        95         GE
        50       MSFT
       100        IBM

The power of this approach lies in its generality. We can use the same print_table() function to print tables for any type of object, as long as we know the names of the attributes we want to display. This makes our table formatter a very useful tool in our programming toolkit.

Understanding Bound Methods in Python

In Python, methods are a special type of attributes that you can call. When you access a method through an object, you're getting what we call a "bound method". A bound method is essentially a method that's tied to a specific object. This means it has access to the object's data and can operate on it.

Accessing Methods as Attributes

Let's start exploring bound methods using our Stock class. First, we'll see how to access a method as an attribute of an object.

## Open a Python interactive shell
python3

## Import the Stock class and create a stock object
from stock import Stock
s = Stock('GOOG', 100, 490.10)

## Access the cost method without calling it
cost_method = s.cost
print(cost_method)  ## Output: <bound method Stock.cost of <stock.Stock object at 0x...>>

## Call the method
result = cost_method()
print(result)  ## Output: 49010.0

## You can also do this in one step
print(s.cost())  ## Output: 49010.0

In the code above, we first import the Stock class and create an instance of it. Then we access the cost method of the s object without actually calling it. This gives us a bound method. When we call this bound method, it calculates the cost based on the object's data. You can also directly call the method on the object in one step.

Using getattr() with Methods

Another way to access methods is by using the getattr() function. This function allows you to get an attribute of an object by its name.

## Get the cost method using getattr
cost_method = getattr(s, 'cost')
print(cost_method)  ## Output: <bound method Stock.cost of <stock.Stock object at 0x...>>

## Call the method
result = cost_method()
print(result)  ## Output: 49010.0

## Get and call in one step
result = getattr(s, 'cost')()
print(result)  ## Output: 49010.0

Here, we use getattr() to get the cost method from the s object. Just like before, we can call the bound method to get the result. And you can even get and call the method in a single line.

The Bound Method and Its Object

A bound method always keeps a reference to the object it was accessed from. This means that even if you store the method in a variable, it still knows which object it belongs to and can access the object's data.

## Store the cost method in a variable
c = s.cost

## Call the method
print(c())  ## Output: 49010.0

## Change the object's state
s.shares = 75

## Call the method again - it sees the updated state
print(c())  ## Output: 36757.5

In this example, we store the cost method in a variable c. When we call c(), it calculates the cost based on the object's current data. Then we change the shares attribute of the s object. When we call c() again, it uses the updated data to calculate the new cost.

Exploring the Bound Method Internals

A bound method has two important attributes that give us more information about it.

  • __self__: This attribute refers to the object the method is bound to.
  • __func__: This attribute is the actual function object that represents the method.
## Get the cost method
c = s.cost

## Examine the bound method attributes
print(c.__self__)  ## Output: <stock.Stock object at 0x...>
print(c.__func__)  ## Output: <function Stock.cost at 0x...>

## You can manually call the function with the object
print(c.__func__(c.__self__))  ## Output: 36757.5 (same as c())

Here, we access the __self__ and __func__ attributes of the bound method c. We can see that __self__ is the s object, and __func__ is the cost function. We can even manually call the function by passing the object as an argument, and it gives us the same result as calling the bound method directly.

Example with a Method that Takes Arguments

Let's see how bound methods work with a method that takes arguments, like the sell() method.

## Get the sell method
sell_method = s.sell

## Examine the method
print(sell_method)  ## Output: <bound method Stock.sell of <stock.Stock object at 0x...>>

## Call the method with an argument
sell_method(25)
print(s.shares)  ## Output: 50

## Call the method manually using __func__ and __self__
sell_method.__func__(sell_method.__self__, 10)
print(s.shares)  ## Output: 40

In this example, we get the sell method as a bound method. When we call it with an argument, it updates the shares attribute of the s object. We can also manually call the method using the __func__ and __self__ attributes, passing the argument as well.

Understanding bound methods helps you comprehend how Python's object system works under the hood, which can be useful for debugging, metaprogramming, and creating advanced programming patterns.

Summary

In this lab, you have learned about Python's attribute access system and its underlying mechanisms. You now know how to access object attributes using dot notation and functions such as getattr(), setattr(), delattr(), and hasattr(). Additionally, you understand how to use getattr() for generic and flexible object processing and how to create a table formatter for any collection of objects.

You have also grasped the concept of bound methods and how they maintain a connection to their objects. These fundamental concepts are crucial for advanced Python programming techniques like introspection, reflection, and metaprogramming. Understanding attribute access enables you to write more flexible and powerful code that can handle various object types.