Home > Articles

This chapter is from the book

This chapter is from the book

5.20 Function Introspection, Attributes, and Signatures

As you have seen, functions are objects—which means they can be assigned to variables, placed in data structures, and used in the same way as any other kind of data in a program. They can also be inspected in various ways. Table 5.1 shows some common attributes of functions. Many of these attributes are useful in debugging, logging, and other operations involving functions.

Table 5.1 Function Attributes

Attribute

Description

f.__name__

Function name

f.__qualname__

Fully qualified name (if nested)

f.__module__

Name of module in which defined

f.__doc__

Documentation string

f.__annotations__

Type hints

f.__globals__

Dictionary that is the global namespace

f.__closure__

Closure variables (if any)

f.__code__

Underlying code object

The f.__name__ attribute contains the name that was used when defining a function. f.__qualname__ is a longer name that includes additional information about the surrounding definition environment.

The f.__module__ attribute is a string that holds the module name in which the function was defined. The f.__globals__ attribute is a dictionary that serves as the global namespace for the function. It is normally the same dictionary that’s attached to the associated module object.

f.__doc__ holds the function documentation string. The f.__annotations__ attribute is a dictionary that holds type hints, if any.

f.__closure__ holds references to the values of closure variables for nested functions. These are a bit buried, but the following example shows how to view them:

def add(x, y):
    def do_add():
        return x + y
    return do_add

>>> a = add(2, 3)
>>> a.__closure__
(<cell at 0x10edf1e20: int object at 0x10ecc1950>,
 <cell at 0x10edf1d90: int object at 0x10ecc1970>)
>>> a.__closure__[0].cell_contents
2
>>>

The f.__code__ object represents the compiled interpreter bytecode for the function body.

Functions can have arbitrary attributes attached to them. Here’s an example:

def func():
    statements

func.secure = 1
func.private = 1

Attributes are not visible within the function body—they are not local variables and do not appear as names in the execution environment. The main use of function attributes is to store extra metadata. Sometimes frameworks or various metaprogramming techniques utilize function tagging—that is, attaching attributes to functions. One example is the @abstractmethod decorator that’s used on methods within abstract base classes. All that decorator does is attach an attribute:

def abstractmethod(func):
    func.__isabstractmethod__ = True
    return func

Some other bit of code (in this case, a metaclass) looks for this attribute and uses it to add extra checks to instance creation.

If you want to know more about a function’s parameters, you can obtain its signature using the inspect.signature() function:

import inspect

def func(x: int, y:float, debug=False) -> float:
    pass

sig = inspect.signature(func)

Signature objects provide many convenient features for printing and obtaining detailed information about the parameters. For example:

# Print out the signature in a nice form
print(sig)  # Produces (x: int, y: float, debug=False) -> float

# Get a list of argument names
print(list(sig.parmeters))   # Produces [ 'x', 'y', 'debug']

# Iterate over the parameters and print various metadata
for p in sig.parameters.values():
    print('name', p.name)
    print('annotation', p.annotation)
    print('kind', p.kind)
    print('default', p.default)

A signature is metadata that describes the nature of a function—how you would call it, type hints, and so on. There are various things that you might do with a signature. One useful operation on signatures is comparison. For example, here’s how you check to see if two functions have the same signature:

def func1(x, y):
    pass

def func2(x, y):
    pass

assert inspect.signature(func1) == inspect.signature(func2)

This kind of comparison might be useful in frameworks. For example, a framework could use signature comparison to see if you’re writing functions or methods that conform to an expected prototype.

If stored in the __signature__ attribute of a function, a signature will be shown in help messages and returned on further uses of inspect.signature(). For example:

def func(x, y, z=None):
    ...

func.__signature__ = inspect.signature(lambda x,y: None)

In this example, the optional argument z would be hidden in further inspection of func. Instead, the attached signature would be returned by inspect.signature().

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.