236 lines
5.5 KiB
Markdown
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
|
||
|
|
||
|
. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
|