\[ [Index](index.md) | [Exercise 6.4](ex6_4.md) | [Exercise 7.1](ex7_1.md) \] # Exercise 6.5 *Objectives:* - Learn how to define a proper callable object Files Modified : `validate.py` Back in [Exercise 4.3](ex4_3.md), you created a series of `Validator` classes for performing different kinds of type and value checks. For example: ```python >>> from validate import Integer >>> Integer.check(1) >>> Integer.check('hello') Traceback (most recent call last): File "", line 1, in File "validate.py", line 21, in check raise TypeError(f'Expected {cls.expected_type}') TypeError: Expected >>> ``` You could use the validators in functions like this: ```python >>> def add(x, y): Integer.check(x) Integer.check(y) return x + y >>> ``` In this exercise, we're going to take it just one step further. ## (a) Creating a Callable Object In the file `validate.py`, start by creating a class like this: ```python # validate.py ... class ValidatedFunction: def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): print('Calling', self.func) result = self.func(*args, **kwargs) return result ``` Test the class by applying it to a function: ```python >>> def add(x, y): return x + y >>> add = ValidatedFunction(add) >>> add(2, 3) Calling 5 >>> ``` ## (b) Enforcement Modify the `ValidatedFunction` class so that it enforces value checks attached via function annotations. For example: ```python >>> def add(x: Integer, y:Integer): return x + y >>> add = ValidatedFunction(add) >>> add(2,3) 5 >>> add('two','three') Traceback (most recent call last): File "", line 1, in File "validate.py", line 67, in __call__ self.func.__annotations__[name].check(val) File "validate.py", line 21, in check raise TypeError(f'Expected {cls.expected_type}') TypeError: expected >>>> ``` Hint: To do this, play around with signature binding. Use the `bind()` method of `Signature` objects to bind function arguments to argument names. Then cross reference this information with the `__annotations__` attribute to get the different validator classes. Keep in mind, you're making an object that looks like a function, but it's really not. There is magic going on behind the scenes. ## (c) Use as a Method (Challenge) A custom callable often presents problems if used as a custom method. For example, try this: ```python class Stock: def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price @property def cost(self): return self.shares * self.price def sell(self, nshares:Integer): self.shares -= nshares sell = ValidatedFunction(sell) # Fails ``` You'll find that the wrapped `sell()` fails miserably: ```python >>> s = Stock('GOOG', 100, 490.1) >>> s.sell(10) Traceback (most recent call last): File "", line 1, in File "validate.py", line 64, in __call__ bound = self.signature.bind(*args, **kwargs) File "/usr/local/lib/python3.6/inspect.py", line 2933, in bind return args[0]._bind(args[1:], kwargs) File "/usr/local/lib/python3.6/inspect.py", line 2848, in _bind raise TypeError(msg) from None TypeError: missing a required argument: 'nshares' >>> ``` Bonus: Figure out why it fails--but don't spend too much time fooling around with it. \[ [Solution](soln6_5.md) | [Index](index.md) | [Exercise 6.4](ex6_4.md) | [Exercise 7.1](ex7_1.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/)