Decorator Chaining and Parameterized Decorators

PythonPythonBeginner
Practice Now

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

Introduction

Objectives:

  • Decorator chaining
  • Defining decorators that accept arguments.

Files Modified: logcall.py, validate.py

Copying Metadata

When a function gets wrapped by a decorator, you often lose information about the name of the function, documentation strings, and other details. Verify this:

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

>>> add
<function wrapper at 0x4439b0>
>>> help(add)
... look at the output ...
>>>

Fix the definition of the logged decorator so that it copies function metadata properly. To do this, use the @wraps(func) decorator as shown in the notes.

After you're done, make sure the decorator preserves the function name and doc string.

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

>>> add
<function add at 0x4439b0>
>>> add.__doc__
'Adds two things'
>>>

Fix the @validated decorator you wrote earlier so that it also preserves metadata using @wraps(func).

âœĻ Check Solution and Practice

Your first decorator with arguments

The @logged decorator you defined earlier always just prints a simple message with the function name. Suppose that you wanted the user to be able to specify a custom message of some sort.

Define a new decorator @logformat(fmt) that accepts a format string as an argument and uses fmt.format(func=func) to format a supplied function into a log message:

## sample.py
...
from logcall import logformat

@logformat('{func.__code__.co_filename}:{func.__name__}')
def mul(x,y):
    return x*y

To do this, you need to define a decorator that takes an argument. This is what it should look like when you test it:

>>> import sample
Adding logging to add
Adding logging to sub
Adding logging to mul
>>> sample.add(2,3)
Calling add
5
>>> sample.mul(2,3)
sample.py:mul
6
>>>

To further simplify the code, show how you can define the original @logged decorator using the the @logformat decorator.

âœĻ Check Solution and Practice

Multiple decorators and methods

Things can get a bit dicey when decorators are applied to methods in a class. Try applying your @logged decorator to the methods in the following class.

class Spam:
    @logged
    def instance_method(self):
        pass

    @logged
    @classmethod
    def class_method(cls):
        pass

    @logged
    @staticmethod
    def static_method():
        pass

    @logged
    @property
    def property_method(self):
        pass

Does it even work at all? (hint: no). Is there any way to fix the code so that it works? For example, can you make it so the following example works?

>>> s = Spam()
>>> s.instance_method()
instance_method
>>> Spam.class_method()
class_method
>>> Spam.static_method()
static_method
>>> s.property_method
property_method
>>>
âœĻ Check Solution and Practice

Validation (Redux)

In the last exercise, you wrote a @validated decorator that enforced type annotations. For example:

@validated
def add(x: Integer, y:Integer) -> Integer:
    return x + y

Make a new decorator @enforce() that enforces types specified via keyword arguments to the decorator instead. For example:

@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
    return x + y

The resulting behavior of the decorated function should be identical. Note: Make the return_ keyword specify the return type. return is a Python reserved word so you have to pick a slightly different name.

Discussion

Writing robust decorators is often a lot harder than it looks. Recommended reading:

âœĻ Check Solution and Practice

Summary

Congratulations! You have completed the Decorator Chaining lab. You can practice more labs in LabEx to improve your skills.

Other Python Tutorials you may like