python-mastery/Exercises/ex4_3.md
2023-07-16 20:21:00 -05:00

236 lines
5.5 KiB
Markdown

\[ [Index](index.md) | [Exercise 4.2](ex4_2.md) | [Exercise 4.4](ex4_4.md) \]
# Exercise 4.3
*Objectives:*
- Learn about descriptors
*Files Created:* `descrip.py`
*Files Modified:* `validate.py`
## (a) Descriptors in action
Earlier, you created a class `Stock` that made use of
slots, properties, and other features. All of these features
are implemented using the descriptor protocol. See it in
action by trying this simple experiment.
First, create a stock object, and try looking up a few attributes:
```python
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.shares
100
>>>
```
Now, notice that these attributes are in the class dictionary.
```python
>>> Stock.__dict__.keys()
['sell', '__module__', '__weakref__', 'price', '_price', 'shares', '_shares',
'__slots__', 'cost', '__repr__', '__doc__', '__init__']
>>>
```
Try these steps which illustrate how descriptors get and set values on an instance:
```python
>>> q = Stock.__dict__['shares']
>>> q.__get__(s)
100
>>> q.__set__(s,75)
>>> s.shares
75
>>> q.__set__(s, '75')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "stock.py", line 23, in shares
raise TypeError('Expected an integer')
TypeError: Expected an integer
>>>
```
The execution of `__get__()` and `__set__()` occurs automatically whenever you access instances.
## (b) Make your own descriptor
Define the descriptor class from the notes:
```python
# descrip.py
class Descriptor:
def __init__(self, name):
self.name = name
def __get__(self, instance, cls):
print('%s:__get__' % self.name)
def __set__(self, instance, value):
print('%s:__set__ %s' % (self.name, value))
def __delete__(self, instance):
print('%s:__delete__' % self.name)
```
Now, try defining a simple class that uses this descriptor:
```python
>>> class Foo:
a = Descriptor('a')
b = Descriptor('b')
c = Descriptor('c')
>>> f = Foo()
>>> f
<__main__.Foo object at 0x38e130> <class __main__.Foo>
>>> f.a
a:__get__
>>> f.b
b:__get__
>>> f.a = 23
a:__set__ 23
>>> del f.a
a:__delete__
>>>
```
Ponder the fact that you have captured the dot-operator for a
specific attribute.
## (c) From Validators to Descriptors
In the previous exercise, you wrote a series of classes that could perform checking.
For example:
```python
>>> PositiveInteger.check(10)
10
>>> PositiveInteger.check('10')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise TypeError('Expected %s' % cls.expected_type)
TypeError: expected <class 'int'>
>>> PositiveInteger.check(-10)
```
You can extend this to descriptors by making a simple change to the `Validator` base
class. Change it to the following:
```python
# validate.py
class Validator:
def __init__(self, name):
self.name = name
@classmethod
def check(cls, value):
return value
def __set__(self, instance, value):
instance.__dict__[self.name] = self.check(value)
```
Note: The lack of the `__get__()` method in the descriptor means that
Python will use its default implementation of attribute lookup. This
requires that the supplied name matches the name used in the instance
dictionary.
No other changes should be necessary. Now, try modifying the `Stock` class to
use the validators as descriptors like this:
```python
class Stock:
name = String('name')
shares = PositiveInteger('shares')
price = PositiveFloat('price')
def __init__(self,name,shares,price):
self.name = name
self.shares = shares
self.price = price
```
You'll find that your class works the same way as before, involves much
less code, and gives you all of the desired checking:
```python
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.shares
100
>>> s.shares = 75
>>> s.shares = '75'
... TypeError ...
>>> s.shares = -50
... ValueError ...
>>>
```
This is pretty cool. Descriptors have allowed you to greatly simplify the implementation
of the `Stock` class. This is the real power of descriptors--you get low level control
over the dot and can use it to do amazing things.
## (d) Fixing the Names
One annoying thing about descriptors is the redundant name specification. For example:
```python
class Stock:
...
shares = PositiveInteger('shares')
...
```
We can fix that. Change the top-level `Validator` class to include a `__set_name__()` method
like this:
```python
# validate.py
class Validator:
def __init__(self, name=None):
self.name = name
def __set_name__(self, cls, name):
self.name = name
@classmethod
def check(cls, value):
return value
def __set__(self, instance, value):
instance.__dict__[self.name] = self.check(value)
```
Now, try rewriting your `Stock` class so that it looks like this:
```python
class Stock:
name = String()
shares = PositiveInteger()
price = PositiveFloat()
def __init__(self,name,shares,price):
self.name = name
self.shares = shares
self.price = price
```
Ah, much nicer. Be aware that this ability to set the name is a Python 3.6
feature however. It won't work on older versions.
\[ [Solution](soln4_3.md) | [Index](index.md) | [Exercise 4.2](ex4_2.md) | [Exercise 4.4](ex4_4.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/)