148 lines
3.8 KiB
Markdown
148 lines
3.8 KiB
Markdown
|
\[ [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 "<stdin>", line 1, in <module>
|
||
|
File "validate.py", line 21, in check
|
||
|
raise TypeError(f'Expected {cls.expected_type}')
|
||
|
TypeError: Expected <class 'int'>
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
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 <function add at 0x1014df598>
|
||
|
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 "<stdin>", line 1, in <module>
|
||
|
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 <class 'int'>
|
||
|
>>>>
|
||
|
```
|
||
|
|
||
|
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 "<stdin>", line 1, in <module>
|
||
|
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
|
||
|
|
||
|
. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
|