Initial commit
This commit is contained in:
235
Exercises/ex4_3.md
Normal file
235
Exercises/ex4_3.md
Normal file
@@ -0,0 +1,235 @@
|
||||
\[ [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/)
|
||||
Reference in New Issue
Block a user