Chapter 7. Function Annotations

Functions and methods can be defined with annotations—expressions that can be used in a function’s signature. Here’s the general syntax:

def functionName(par1 : exp1par2 : exp2, ..., parN : expN)-> rexp:
    suite


Every colon expression part (: expX) is an optional annotation, and so is the arrow return expression part (-> rexp). The last (or only) positional parameter (if present) can be of the form *args, with or without an annotation; similarly, the last (or only) keyword parameter (if present) can be of the form **kwargs, again with or without an annotation.

If annotations are present they are added to the function’s __annotations__ dictionary; if they are not present this dictionary is empty. The dictionary’s keys are the parameter names, and the values are the corresponding expressions. The syntax allows us to annotate all, some, or none of the parameters and to annotate the return value or not. Annotations have no special significance to Python. The only thing that Python does in the face of annotations is to put them in the __annotations__ dictionary; any other action is up to us. Here is an example of an annotated function that is in the Util module:

def is_unicode_punctuation(s : str) -> bool:
    for c in s:
        if unicodedata.category(c)[0] != "P":
            return False
    return True


Every Unicode character belongs to a particular category and each category is identified by a two-character identifier. All the categories that begin with P are punctuation characters.

Here we have used Python data types as the annotation expressions. But they have no particular meaning for Python, as these calls should make clear:

image

The first call uses a positional argument and the second call a keyword argument, just to show that both kinds work as expected. The last call passes a tuple rather than a string, and this is accepted since Python does nothing more than record the annotations in the __annotations__ dictionary.

If we want to give meaning to annotations, for example, to provide type checking, one approach is to decorate the functions we want the meaning to apply to with a suitable decorator. Here is a very basic type-checking decorator:

def strictly_typed(function):
    annotations = function.__annotations__
    arg_spec = inspect.getfullargspec(function)

    assert "return" in annotations, "missing type for return value"
    for arg in arg_spec.args + arg_spec.kwonlyargs:
        assert arg in annotations, ("missing type for parameter '" +
                                    arg + "'")
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        for name, arg in (list(zip(arg_spec.args, args)) +
                          list(kwargs.items())):
            assert isinstance(arg, annotations[name]), (
                    "expected argument '{0}' of {1} got {2}".format(
                    name, annotations[name], type(arg)))
        result = function(*args, **kwargs)
        assert isinstance(result, annotations["return"]), (
                    "expected return of {0} got {1}".format(
                    annotations["return"], type(result)))
        return result
    return wrapper


This decorator requires that every argument and the return value must be annotated with the expected type. It checks that the function’s arguments and return type are all annotated with their types when the function it is passed is created, and at runtime it checks that the types of the actual arguments match those expected.

The inspect module provides powerful introspection services for objects. Here, we have made use of only a small part of the argument specification object it returns, to get the names of each positional and keyword argument—in the correct order in the case of the positional arguments. These names are then used in conjunction with the annotations dictionary to ensure that every parameter and the return value are annotated.

The wrapper function created inside the decorator begins by iterating over every name–argument pair of the given positional and keyword arguments. Since zip() returns an iterator and dictionary.items() returns a dictionary view we cannot concatenate them directly, so first we convert them both to lists. If any actual argument has a different type from its corresponding annotation the assertion will fail; otherwise, the actual function is called and the type of the value returned is checked, and if it is of the right type, it is returned. At the end of the strictly_typed() function, we return the wrapped function as usual.

Notice that the checking is done only in debug mode (which is Python’s default mode—controlled by the -O command-line option and the PYTHONOPTIMIZE environment variable).

If we decorate the is_unicode_punctuation() function with the @strictly_typed decorator, and try the same examples as before using the decorated version, the annotations are acted upon:

image

Now the argument types are checked, so in the last case an AssertionError is raised because a tuple is not a string or a subclass of str.

Now we will look at a completely different use of annotations. Here’s a small function that has the same functionality as the built-in range() function, except that it always returns floats:

def range_of_floats(*args) -> "author=Reginald Perrin":
    return (float(x) for x in range(*args))


No use is made of the annotation by the function itself, but it is easy to envisage a tool that imported all of a project’s modules and produced a list of function names and author names, extracting each function’s name from its __name__ attribute, and the author names from the value of the __annotations__ dictionary’s "return" item.

Annotations are a very new feature of Python, and because Python does not impose any predefined meaning on them, the uses they can be put to are limited only by our imagination. Further ideas for possible uses, and some useful links, are available from PEP 3107 “Function Annotations”, www.python.org/dev/peps/pep-3107.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset