\[ [Index](index.md) | [Exercise 7.1](ex7_1.md) | [Exercise 7.3](ex7_3.md) \] # 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: ```python >>> @logged def add(x,y): 'Adds two things' return x+y >>> add >>> 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. ```python >>> @logged def add(x,y): 'Adds two things' return x+y >>> add >>> 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: ```python # 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: ```python >>> 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. ```python 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? ```python >>> 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: ```python @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: ```python @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: \[ [Solution](soln7_2.md) | [Index](index.md) | [Exercise 7.1](ex7_1.md) | [Exercise 7.3](ex7_3.md) \] ---- `>>>` Advanced Python Mastery `...` A course by [dabeaz](https://www.dabeaz.com) `...` Copyright 2007-2023 ![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)