Inspect the Internals of Functions

Beginner

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

Introduction

In this lab, you will learn how to explore the internals of Python functions. Functions in Python are objects with their own attributes and methods, and understanding these can offer deeper insights into how functions operate. This knowledge will enable you to write more powerful and adaptable code.

You will learn to inspect function attributes and properties, use the inspect module to examine function signatures, and apply these inspection techniques to enhance a class implementation. The file you will modify is structure.py.

Exploring Function Attributes

In Python, functions are considered first-class objects. What does this mean? Well, it's similar to how you have different types of objects in the real world, like a book or a pen. In Python, functions are also objects, and just like other objects, they come with their own set of attributes. These attributes can give us a lot of useful information about the function, such as its name, where it's defined, and how it's implemented.

Let's start our exploration by opening a Python interactive shell. This shell is like a playground where we can write and run Python code right away. To do this, we'll first navigate to the project directory and then start the Python interpreter. Here are the commands to run in your terminal:

cd ~/project
python3

Now that we're in the Python interactive shell, let's define a simple function. This function will take two numbers and add them together. Here's how we can define it:

def add(x, y):
    'Adds two things'
    return x + y

In this code, we've created a function named add. It takes two parameters, x and y, and returns their sum. The string 'Adds two things' is called a docstring, which is used to document what the function does.

Using dir() to Inspect Function Attributes

In Python, the dir() function is a handy tool. It can be used to get a list of all the attributes and methods that an object has. Let's use it to see what attributes our add function has. Run the following code in the Python interactive shell:

dir(add)

When you run this code, you'll see a long list of attributes. Here's an example of what the output might look like:

['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

This list shows all the attributes and methods associated with the add function.

Accessing Basic Function Information

Now, let's take a closer look at some of the basic function attributes. These attributes can tell us important information about the function. Run the following code in the Python interactive shell:

print(add.__name__)
print(add.__module__)
print(add.__doc__)

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

add
__main__
Adds two things

Let's understand what each of these attributes means:

  • __name__: This attribute gives us the name of the function. In our case, the function is named add.
  • __module__: It tells us the module where the function is defined. When we run code in the interactive shell, the module is usually __main__.
  • __doc__: This is the function's documentation string, or docstring. It provides a brief description of what the function does.

Examining Function Code

The __code__ attribute of a function is very interesting. It contains information about how the function is implemented, including its bytecode and other details. Let's see what we can learn from it. Run the following code in the Python interactive shell:

print(add.__code__.co_varnames)
print(add.__code__.co_argcount)

The output will be:

('x', 'y')
2

Here's what these attributes tell us:

  • co_varnames: It's a tuple that contains the names of all the local variables used by the function. In our add function, the local variables are x and y.
  • co_argcount: This attribute tells us the number of arguments that the function expects. Our add function expects two arguments, so the value is 2.

If you're curious to explore more attributes of the __code__ object, you can use the dir() function again. Run the following code:

dir(add.__code__)

This will display all the attributes of the code object, which contain low-level details about how the function is implemented.

Using the inspect Module

In Python, the standard library comes with a very useful inspect module. This module is like a detective tool that helps us gather information about live objects in Python. Live objects can be things like modules, classes, and functions. Instead of manually digging through an object's attributes to find information, the inspect module provides more organized and high - level ways to understand the properties of functions.

Let's keep using the same Python interactive shell to explore how this module works.

Function Signatures

The inspect.signature() function is a handy tool. When you pass a function to it, it returns a Signature object. This object holds important details about the function's parameters.

Here's an example. Suppose we have a function named add. We can use the inspect.signature() function to get its signature:

import inspect
sig = inspect.signature(add)
print(sig)

When you run this code, the output will be:

(x, y)

This output shows us the function's signature, which tells us what parameters the function can accept.

Examining Parameter Details

We can go a step further and get more in - depth information about each parameter of the function.

print(sig.parameters)

The output of this code will be:

OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">)])

The parameters of the function are stored in an ordered dictionary. Sometimes, we might only be interested in the names of the parameters. We can convert this ordered dictionary to a tuple to extract just the parameter names.

param_names = tuple(sig.parameters)
print(param_names)

The output will be:

('x', 'y')

Examining Individual Parameters

We can also take a closer look at each individual parameter. The following code loops through each parameter in the function and prints out some important details about it.

for name, param in sig.parameters.items():
    print(f"Parameter: {name}")
    print(f"  Kind: {param.kind}")
    print(f"  Default: {param.default if param.default is not param.empty else 'No default'}")

This code will show us details about each parameter. It tells us the kind of the parameter (whether it's a positional parameter, a keyword parameter, etc.) and its default value if it has one.

The inspect module has many other useful functions for function introspection. Here are some examples:

  • inspect.getdoc(obj): This function retrieves the documentation string for an object. Documentation strings are like notes that programmers write to explain what an object does.
  • inspect.getfile(obj): It helps us find out the file where an object is defined. This can be very useful when we want to locate the source code of an object.
  • inspect.getsource(obj): This function fetches the source code of an object. It allows us to see exactly how the object is implemented.

Applying Function Inspection in Classes

Now, we're going to take what we've learned about function inspection and use it to improve a class implementation. Function inspection allows us to look inside functions and understand their structure, like the parameters they take. In this case, we'll use it to make our class code more efficient and less error - prone. We'll modify a Structure class so that it can automatically detect field names from the __init__ method signature.

Understanding the Structure Class

The structure.py file contains a Structure class. This class acts as a base class, which means other classes can inherit from it to create structured data objects. Currently, to define the attributes of the objects created from classes inheriting from Structure, we need to set a _fields class variable.

Let's open the file in the editor. We'll use the following command to navigate to the project directory:

cd ~/project

Once you've run this command, you can find and view the existing Structure class in the structure.py file within the WebIDE.

Creating a Stock Class

Let's create a Stock class that inherits from the Structure class. Inheritance means that the Stock class will get all the features of the Structure class and can also add its own. We'll add the following code to the end of the structure.py file:

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

    def __init__(self, name, shares, price):
        self._init()

However, there's a problem with this approach. We have to define both the _fields tuple and the __init__ method with the same parameter names. This is redundant because we're essentially writing the same information twice. If we forget to update one when we change the other, it can lead to errors.

Adding a set_fields Class Method

To fix this issue, we'll add a set_fields class method to the Structure class. This method will automatically detect the field names from the __init__ signature. Here's the code we need to add to the Structure class:

@classmethod
def set_fields(cls):
    ## Get the signature of the __init__ method
    import inspect
    sig = inspect.signature(cls.__init__)

    ## Get parameter names, skipping 'self'
    params = list(sig.parameters.keys())[1:]

    ## Set _fields attribute on the class
    cls._fields = tuple(params)

This method uses the inspect module, which is a powerful tool in Python for getting information about objects like functions and classes. First, it gets the signature of the __init__ method. Then, it extracts the parameter names, but skips the self parameter because self is a special parameter in Python classes that refers to the instance itself. Finally, it sets the _fields class variable with these parameter names.

Modifying the Stock Class

Now that we have the set_fields method, we can simplify our Stock class. Replace the previous Stock class code with the following:

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

## Call set_fields to automatically set _fields from __init__
Stock.set_fields()

This way, we don't have to manually define the _fields tuple. The set_fields method will take care of it for us.

Testing the Modified Class

To make sure our modified class works correctly, we'll create a simple test script. Create a new file called test_structure.py and add the following code:

from structure import Stock

def test_stock():
    ## Create a Stock object
    s = Stock(name='GOOG', shares=100, price=490.1)

    ## Test string representation
    print(f"Stock representation: {s}")

    ## Test attribute access
    print(f"Name: {s.name}")
    print(f"Shares: {s.shares}")
    print(f"Price: {s.price}")

    ## Test attribute modification
    s.shares = 50
    print(f"Updated shares: {s.shares}")

    ## Test attribute error
    try:
        s.share = 50  ## Misspelled attribute
        print("Error: Did not raise AttributeError")
    except AttributeError as e:
        print(f"Correctly raised: {e}")

if __name__ == "__main__":
    test_stock()

This test script creates a Stock object, tests its string representation, accesses its attributes, modifies an attribute, and tries to access a misspelled attribute to check if it raises the correct error.

To run the test script, use the following command:

python3 test_structure.py

You should see output similar to this:

Stock representation: Stock('GOOG',100,490.1)
Name: GOOG
Shares: 100
Price: 490.1
Updated shares: 50
Correctly raised: No attribute share

How It Works

  1. The set_fields method uses inspect.signature() to get the parameter names from the __init__ method. This function gives us detailed information about the parameters of the __init__ method.
  2. It then automatically sets the _fields class variable based on these parameter names. So, we don't have to write the same parameter names in two different places.
  3. This eliminates the need to manually define both _fields and __init__ with matching parameter names. It makes our code more maintainable because if we change the parameters in the __init__ method, the _fields will be updated automatically.

This approach uses function inspection to make our code more maintainable and less error - prone. It's a practical application of Python's introspection capabilities, which allow us to examine and modify objects at runtime.

Summary

In this lab, you have learned how to inspect the internals of Python functions. You examined function attributes directly using methods like dir() and accessed special attributes such as __name__, __doc__, and __code__. You also used the inspect module to obtain structured information about function signatures and parameters.

Function inspection is a powerful Python feature that allows you to write more dynamic, flexible, and maintainable code. The ability to examine and manipulate function properties at runtime offers possibilities for metaprogramming, creating self - documenting code, and building advanced frameworks.