Skip to content

Latest commit

 

History

History
177 lines (136 loc) · 4.16 KB

ex7_2.md

File metadata and controls

177 lines (136 loc) · 4.16 KB

[ Index | Exercise 7.1 | Exercise 7.3 ]

Exercise 7.2

Objectives:

  • Decorator chaining
  • Defining decorators that accept arguments.

Files Modified: logcall.py, validate.py

(a) 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).

(b) 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.

(c) 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
>>>

(d) 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: How you implemented your Python decorator is wrong

[ Solution | Index | Exercise 7.1 | Exercise 7.3 ]


>>> Advanced Python Mastery
... A course by dabeaz
... Copyright 2007-2023

. This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License