289 lines
7.4 KiB
Markdown
289 lines
7.4 KiB
Markdown
|
\[ [Index](index.md) | [Exercise 4.1](ex4_1.md) | [Exercise 4.3](ex4_3.md) \]
|
||
|
|
||
|
# Exercise 4.2
|
||
|
|
||
|
*Objectives:*
|
||
|
|
||
|
- Learn more about the behavior of inheritance
|
||
|
- Understand the behavior of super().
|
||
|
- More cooperative inheritance.
|
||
|
|
||
|
*Files Created:* `validate.py`
|
||
|
|
||
|
|
||
|
## (a) The directions of inheritance
|
||
|
|
||
|
Python has two different "directions" of inheritance. The first is
|
||
|
found in the concept of "single inheritance" where a series
|
||
|
of classes inherit from a single parent. For example, try this example:
|
||
|
|
||
|
```python
|
||
|
>>> class A:
|
||
|
def spam(self):
|
||
|
print('A.spam')
|
||
|
|
||
|
>>> class B(A):
|
||
|
def spam(self):
|
||
|
print('B.spam')
|
||
|
super().spam()
|
||
|
|
||
|
>>> class C(B):
|
||
|
def spam(self):
|
||
|
print('C.spam')
|
||
|
super().spam()
|
||
|
|
||
|
|
||
|
>>> C.__mro__
|
||
|
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
|
||
|
>>> c = C()
|
||
|
>>> c.spam()
|
||
|
C.spam
|
||
|
B.spam
|
||
|
A.spam
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
Observe that the `__mro__` attribute of class `C` encodes all of its ancestors in
|
||
|
order. When you invoke the `spam()` method, it walks the MRO class-by-class up
|
||
|
the hierarchy.
|
||
|
|
||
|
With multiple inheritance, you get a different kind of inheritance that
|
||
|
allows different classes to be composed together. Try this example:
|
||
|
|
||
|
```
|
||
|
>>> class Base:
|
||
|
def spam(self):
|
||
|
print('Base.spam')
|
||
|
|
||
|
>>> class X(Base):
|
||
|
def spam(self):
|
||
|
print('X.spam')
|
||
|
super().spam()
|
||
|
|
||
|
>>> class Y(Base):
|
||
|
def spam(self):
|
||
|
print('Y.spam')
|
||
|
super().spam()
|
||
|
|
||
|
>>> class Z(Base):
|
||
|
def spam(self):
|
||
|
print('Z.spam')
|
||
|
super().spam()
|
||
|
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
Notice that all of the classes above inherit from a common parent `Base`.
|
||
|
However, the classes `X`, `Y`, and `Z` are not directly related
|
||
|
to each other (there is no inheritance chain linking those classes together).
|
||
|
|
||
|
However, watch what happens in multiple inheritance:
|
||
|
|
||
|
```python
|
||
|
>>> class M(X,Y,Z):
|
||
|
pass
|
||
|
|
||
|
>>> M.__mro__
|
||
|
(<class '__main__.M'>, <class '__main__.X'>, <class '__main__.Y'>, <class '__main__.Z'>, <class '__main__.Base'>, <class 'object'>)
|
||
|
>>> m = M()
|
||
|
>>> m.spam()
|
||
|
X.spam
|
||
|
Y.spam
|
||
|
Z.spam
|
||
|
Base.spam
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
Here, you see all of the classes stack together in the order supplied by the subclass.
|
||
|
Suppose the subclass rearranges the class order:
|
||
|
|
||
|
```python
|
||
|
>>> class N(Z,Y,X):
|
||
|
pass
|
||
|
|
||
|
>>> N.__mro__
|
||
|
(<class '__main__.N'>, <class '__main__.Z'>, <class '__main__.Y'>, <class '__main__.X'>, <class '__main__.Base'>, <class 'object'>)
|
||
|
>>> n = N()
|
||
|
>>> n.spam()
|
||
|
Z.spam
|
||
|
Y.spam
|
||
|
X.spam
|
||
|
Base.spam
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
Here, you see the order of the parents flip around. Carefully pay attention to what `super()`
|
||
|
is doing in both cases. It doesn't delegate to the immediate parent of each class--instead,
|
||
|
it moves to the next class on the MRO. Not only that, the exact order is controlled
|
||
|
by the child. This is pretty weird.
|
||
|
|
||
|
Also notice that the common parent `Base` serves to terminate the chain of `super()` operations.
|
||
|
Specifically, the `Base.spam()` method does not call any further methods. It also appears at
|
||
|
the end of the MRO since it is the parent to all of the classes being composed together.
|
||
|
|
||
|
## (b) Build a Value Checker
|
||
|
|
||
|
In link:ex3_4.html[Exercise 3.4], you added some properties to the `Stock` class that
|
||
|
checked attributes for different types and values (e.g., shares had to be a positive
|
||
|
integer). Let's play with that idea a bit. Start by creating a file `validate.py` and
|
||
|
defining the following base class:
|
||
|
|
||
|
```python
|
||
|
# validate.py
|
||
|
class Validator:
|
||
|
@classmethod
|
||
|
def check(cls, value):
|
||
|
return value
|
||
|
```
|
||
|
|
||
|
Now, let's make some classes for type checking:
|
||
|
|
||
|
```python
|
||
|
class Typed(Validator):
|
||
|
expected_type = object
|
||
|
@classmethod
|
||
|
def check(cls, value):
|
||
|
if not isinstance(value, cls.expected_type):
|
||
|
raise TypeError(f'Expected {cls.expected_type}')
|
||
|
return super().check(value)
|
||
|
|
||
|
class Integer(Typed):
|
||
|
expected_type = int
|
||
|
|
||
|
class Float(Typed):
|
||
|
expected_type = float
|
||
|
|
||
|
class String(Typed):
|
||
|
expected_type = str
|
||
|
```
|
||
|
|
||
|
Here's how you use these classes (Note: the use of `@classmethod` allows us to
|
||
|
avoid the extra step of creating instances which we don't really need):
|
||
|
|
||
|
```python
|
||
|
>>> Integer.check(10)
|
||
|
10
|
||
|
>>> Integer.check('10')
|
||
|
Traceback (most recent call last):
|
||
|
File "<stdin>", line 1, in check
|
||
|
raise TypeError(f'Expected {cls.expected_type}')
|
||
|
TypeError: Expected <class 'int'>
|
||
|
>>> String.check('10')
|
||
|
'10'
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
You could use the validators in a function. For example:
|
||
|
|
||
|
```python
|
||
|
>>> def add(x, y):
|
||
|
Integer.check(x)
|
||
|
Integer.check(y)
|
||
|
return x + y
|
||
|
|
||
|
>>> add(2, 2)
|
||
|
4
|
||
|
>>> add('2', '3')
|
||
|
Traceback (most recent call last):
|
||
|
File "<stdin>", line 1, in <module>
|
||
|
File "<stdin>", line 2, in add
|
||
|
File "validate.py", line 11, in check
|
||
|
raise TypeError(f'Expected {cls.expected_type}')
|
||
|
TypeError: Expected <class 'int'>
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
Now, make some more classes for different kinds of domain checking:
|
||
|
|
||
|
```python
|
||
|
class Positive(Validator):
|
||
|
@classmethod
|
||
|
def check(cls, value):
|
||
|
if value < 0:
|
||
|
raise ValueError('Expected >= 0')
|
||
|
return super().check(value)
|
||
|
|
||
|
class NonEmpty(Validator):
|
||
|
@classmethod
|
||
|
def check(cls, value):
|
||
|
if len(value) == 0:
|
||
|
raise ValueError('Must be non-empty')
|
||
|
return super().check(value)
|
||
|
```
|
||
|
|
||
|
Where is all of this going? Let's start composing classes together with multiple inheritance like toy blocks:
|
||
|
|
||
|
```python
|
||
|
class PositiveInteger(Integer, Positive):
|
||
|
pass
|
||
|
|
||
|
class PositiveFloat(Float, Positive):
|
||
|
pass
|
||
|
|
||
|
class NonEmptyString(String, NonEmpty):
|
||
|
pass
|
||
|
```
|
||
|
|
||
|
Essentially, you're taking existing validators and composing them
|
||
|
together into new ones. Madness! However, let's use them to validate
|
||
|
some things now:
|
||
|
|
||
|
```python
|
||
|
>>> PositiveInteger.check(10)
|
||
|
10
|
||
|
>>> PositiveInteger.check('10')
|
||
|
Traceback (most recent call last):
|
||
|
File "<stdin>", line 1, in <module>
|
||
|
raise TypeError(f'Expected {cls.expected_type}')
|
||
|
TypeError: Expected <class 'int'>
|
||
|
>>> PositiveInteger.check(-10)
|
||
|
Traceback (most recent call last):
|
||
|
File "<stdin>", line 1, in <module>
|
||
|
raise ValueError('Expected >= 0')
|
||
|
ValueError: Must be >= 0
|
||
|
|
||
|
|
||
|
>>> NonEmptyString.check('hello')
|
||
|
'hello'
|
||
|
>>> NonEmptyString.check('')
|
||
|
Traceback (most recent call last):
|
||
|
File "<stdin>", line 1, in <module>
|
||
|
raise ValueError('Must be non-empty')
|
||
|
ValueError: Must be non-empty
|
||
|
>>>
|
||
|
```
|
||
|
|
||
|
At this point, your head is probably fully exploded. However, the problem of composing
|
||
|
different bits of code together is one that arises in real-world programs. Cooperative
|
||
|
multiple inheritance is one of the tools that can be used to organize it.
|
||
|
|
||
|
## (c) Using your validators
|
||
|
|
||
|
Your validators can be used to add value checking to functions and classes. For
|
||
|
example, perhaps the validators could be used in the properties of `Stock`:
|
||
|
|
||
|
```python
|
||
|
class Stock:
|
||
|
...
|
||
|
@property
|
||
|
def shares(self):
|
||
|
return self._shares
|
||
|
|
||
|
@shares.setter
|
||
|
def shares(self, value):
|
||
|
self._shares = PositiveInteger.check(value)
|
||
|
...
|
||
|
```
|
||
|
|
||
|
Copy the `Stock` class from `stock.py` change it to use the validators in the property
|
||
|
code for `shares` and `price`.
|
||
|
|
||
|
\[ [Solution](soln4_2.md) | [Index](index.md) | [Exercise 4.1](ex4_1.md) | [Exercise 4.3](ex4_3.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/)
|