Initial commit
This commit is contained in:
449
Exercises/ex2_4.md
Normal file
449
Exercises/ex2_4.md
Normal file
@@ -0,0 +1,449 @@
|
||||
\[ [Index](index.md) | [Exercise 2.3](ex2_3.md) | [Exercise 2.5](ex2_5.md) \]
|
||||
|
||||
# Exercise 2.4
|
||||
|
||||
*Objectives:*
|
||||
|
||||
- Make a new primitive type
|
||||
|
||||
In most programs, you use the primitive types such as `int`, `float`,
|
||||
and `str` to represent data. However, you're not limited to just those
|
||||
types. The standard library has modules such as the `decimal` and
|
||||
`fractions` module that implement new primitive types. You can also
|
||||
make your own types as long as you understand the underlying protocols
|
||||
which make Python objects work. In this exercise, we'll make a new
|
||||
primitive type. There are a lot of little details to worry about, but
|
||||
this will give you a general sense for what's required.
|
||||
|
||||
## (a) Mutable Integers
|
||||
|
||||
Python integers are normally immutable. However, suppose you wanted to
|
||||
make a mutable integer object. Start off by making a class like this:
|
||||
|
||||
```python
|
||||
# mutint.py
|
||||
|
||||
class MutInt:
|
||||
__slots__ = ['value']
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
```
|
||||
|
||||
Try it out:
|
||||
|
||||
```python
|
||||
>>> a = MutInt(3)
|
||||
>>> a
|
||||
<__main__.MutInt object at 0x10e79d408>
|
||||
>>> a.value
|
||||
3
|
||||
>>> a.value = 42
|
||||
>>> a.value
|
||||
42
|
||||
>>> a + 10
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
TypeError: unsupported operand type(s) for +: 'MutInt' and 'int'
|
||||
>>>
|
||||
```
|
||||
|
||||
That's all very exciting except that nothing really works with this
|
||||
new `MutInt` object. Printing is horrible, none of the math
|
||||
operators work, and it's basically rather useless. Well, except for
|
||||
the fact that its value is mutable--it does have that.
|
||||
|
||||
## (b) Fixing output
|
||||
|
||||
You can fix output by giving the object methods such as `__str__()`,
|
||||
`__repr__()`, and `__format__()`. For example:
|
||||
|
||||
```python
|
||||
# mint.py
|
||||
|
||||
class MutInt:
|
||||
__slots__ = ['value']
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return f'MutInt({self.value!r})'
|
||||
|
||||
def __format__(self, fmt):
|
||||
return format(self.value, fmt)
|
||||
```
|
||||
|
||||
Try it out:
|
||||
|
||||
```python
|
||||
>>> a = MutInt(3)
|
||||
>>> print(a)
|
||||
3
|
||||
>>> a
|
||||
MutInt(3)
|
||||
>>> f'The value is {a:*^10d}'
|
||||
The value is ****3*****
|
||||
>>> a.value = 42
|
||||
>>> a
|
||||
MutInt(42)
|
||||
>>>
|
||||
```
|
||||
|
||||
## (c) Math Operators
|
||||
|
||||
You can make an object work with various math operators if you implement the
|
||||
appropriate methods for it. However, it's your responsibility to
|
||||
recognize other types of data and implement the appropriate conversion
|
||||
code. Modify the `MutInt` class by giving it an `__add__()` method
|
||||
as follows:
|
||||
|
||||
```python
|
||||
class MutInt:
|
||||
__slots__ = ['value']
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
...
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, MutInt):
|
||||
return MutInt(self.value + other.value)
|
||||
elif isinstance(other, int):
|
||||
return MutInt(self.value + other)
|
||||
else:
|
||||
return NotImplemented
|
||||
```
|
||||
|
||||
With this change, you should find that you can add both integers and
|
||||
mutable integers. The result is a `MutInt` instance. Adding
|
||||
other kinds of numbers results in an error:
|
||||
|
||||
```python
|
||||
>>> a = MutInt(3)
|
||||
>>> b = a + 10
|
||||
>>> b
|
||||
MutInt(13)
|
||||
>>> b.value = 23
|
||||
>>> c = a + b
|
||||
>>> c
|
||||
MutInt(26)
|
||||
>>> a + 3.5
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
TypeError: unsupported operand type(s) for +: 'MutInt' and 'float'
|
||||
>>>
|
||||
```
|
||||
|
||||
One problem with the code is that it doesn't work when the order of operands
|
||||
is reversed. Consider:
|
||||
|
||||
```python
|
||||
>>> a + 10
|
||||
MutInt(13)
|
||||
>>> 10 + a
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
TypeError: unsupported operand type(s) for +: 'int' and 'MutInt'
|
||||
>>>
|
||||
```
|
||||
|
||||
This is occurring because the `int` type has no knowledge of `MutInt`
|
||||
and it's confused. This can be fixed by adding an `__radd__()` method. This
|
||||
method is called if the first attempt to call `__add__()` didn't work with the
|
||||
provided object.
|
||||
|
||||
```python
|
||||
class MutInt:
|
||||
__slots__ = ['value']
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
...
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, MutInt):
|
||||
return MutInt(self.value + other.value)
|
||||
elif isinstance(other, int):
|
||||
return MutInt(self.value + other)
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
__radd__ = __add__ # Reversed operands
|
||||
```
|
||||
|
||||
With this change, you'll find that addition works:
|
||||
|
||||
```python
|
||||
>>> a = MutInt(3)
|
||||
>>> a + 10
|
||||
MutInt(13)
|
||||
>>> 10 + a
|
||||
MutInt(13)
|
||||
>>>
|
||||
```
|
||||
|
||||
Since our integer is mutable, you can also make it recognize the in-place
|
||||
add-update operator `+=` by implementing the `__iadd__()` method:
|
||||
|
||||
```python
|
||||
class MutInt:
|
||||
__slots__ = ['value']
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
...
|
||||
|
||||
def __iadd__(self, other):
|
||||
if isinstance(other, MutInt):
|
||||
self.value += other.value
|
||||
return self
|
||||
elif isinstance(other, int):
|
||||
self.value += other
|
||||
return self
|
||||
else:
|
||||
return NotImplemented
|
||||
```
|
||||
|
||||
This allows for interesting uses like this:
|
||||
|
||||
```python
|
||||
>>> a = MutInt(3)
|
||||
>>> b = a
|
||||
>>> a += 10
|
||||
>>> a
|
||||
MutInt(13)
|
||||
>>> b # Notice that b also changes
|
||||
MutInt(13)
|
||||
>>>
|
||||
```
|
||||
|
||||
That might seem kind of strange that `b` also changes, but there are subtle features like
|
||||
this with built-in Python objects. For example:
|
||||
|
||||
```python
|
||||
>>> a = [1,2,3]
|
||||
>>> b = a
|
||||
>>> a += [4,5]
|
||||
>>> a
|
||||
[1, 2, 3, 4, 5]
|
||||
>>> b
|
||||
[1, 2, 3, 4, 5]
|
||||
|
||||
>>> c = (1,2,3)
|
||||
>>> d = c
|
||||
>>> c += (4,5)
|
||||
>>> c
|
||||
(1, 2, 3, 4, 5)
|
||||
>>> d # Explain difference from lists
|
||||
(1, 2, 3)
|
||||
>>>
|
||||
```
|
||||
|
||||
## (d) Comparisons
|
||||
|
||||
One problem is that comparisons still don't work. For example:
|
||||
|
||||
```python
|
||||
>>> a = MutInt(3)
|
||||
>>> b = MutInt(3)
|
||||
>>> a == b
|
||||
False
|
||||
>>> a == 3
|
||||
False
|
||||
>>>
|
||||
```
|
||||
|
||||
You can fix this by adding an `__eq__()` method. Further methods such
|
||||
as `__lt__()`, `__le__()`, `__gt__()`, `__ge__()` can be used to
|
||||
implement other comparisons. For example:
|
||||
|
||||
```python
|
||||
class MutInt:
|
||||
__slots__ = ['value']
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
...
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, MutInt):
|
||||
return self.value == other.value
|
||||
elif isinstance(other, int):
|
||||
return self.value == other
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, MutInt):
|
||||
return self.value < other.value
|
||||
elif isinstance(other, int):
|
||||
return self.value < other
|
||||
else:
|
||||
return NotImplemented
|
||||
```
|
||||
|
||||
Try it:
|
||||
|
||||
```python
|
||||
>>> a = MutInt(3)
|
||||
>>> b = MutInt(3)
|
||||
>>> a == b
|
||||
True
|
||||
>>> c = MutInt(4)
|
||||
>>> a < c
|
||||
True
|
||||
>>> a <= c
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
TypeError: '<=' not supported between instances of 'MutInt' and 'MutInt'
|
||||
>>>
|
||||
```
|
||||
|
||||
The reason the `<=` operator is failing is that no `__le__()` method was provided.
|
||||
You could code it separately, but an easier way to get it is to use the `@total_ordering`
|
||||
decorator:
|
||||
|
||||
```python
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class MutInt:
|
||||
__slots__ = ['value']
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
...
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, MutInt):
|
||||
return self.value == other.value
|
||||
elif isinstance(other, int):
|
||||
return self.value == other
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, MutInt):
|
||||
return self.value < other.value
|
||||
elif isinstance(other, int):
|
||||
return self.value < other
|
||||
else:
|
||||
return NotImplemented
|
||||
```
|
||||
|
||||
`@total_ordering` fills in the missing comparison methods for you as long as
|
||||
you minimally provide an equality operator and one of the other relations.
|
||||
|
||||
## (e) Conversions
|
||||
|
||||
Your new primitive type is almost complete. You might want to give it
|
||||
the ability to work with some common conversions. For example:
|
||||
|
||||
```python
|
||||
>>> a = MutInt(3)
|
||||
>>> int(a)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
TypeError: int() argument must be a string, a bytes-like object or a number, not 'MutInt'
|
||||
>>> float(a)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
TypeError: float() argument must be a string, a bytes-like object or a number, not 'MutInt'
|
||||
>>>
|
||||
```
|
||||
|
||||
You can give your class an `__int__()` and `__float__()` method to fix this:
|
||||
|
||||
```python
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class MutInt:
|
||||
__slots__ = ['value']
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
...
|
||||
|
||||
def __int__(self):
|
||||
return self.value
|
||||
|
||||
def __float__(self):
|
||||
return float(self.value)
|
||||
```
|
||||
|
||||
Now, you can properly convert:
|
||||
|
||||
```python
|
||||
>>> a = MutInt(3)
|
||||
>>> int(a)
|
||||
3
|
||||
>>> float(a)
|
||||
3.0
|
||||
>>>
|
||||
```
|
||||
|
||||
As a general rule, Python never automatically converts data though. Thus, even though you
|
||||
gave the class an `__int__()` method, `MutInt` is still not going to work in all
|
||||
situations when an integer might be expected. For example, indexing:
|
||||
|
||||
```python
|
||||
>>> names = ['Dave', 'Guido', 'Paula', 'Thomas', 'Lewis']
|
||||
>>> a = MutInt(1)
|
||||
>>> names[a]
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
TypeError: list indices must be integers or slices, not MutInt
|
||||
>>>
|
||||
```
|
||||
|
||||
This can be fixed by giving `MutInt` an `__index__()` method that produces an integer.
|
||||
Modify the class like this:
|
||||
|
||||
```python
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class MutInt:
|
||||
__slots__ = ['value']
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
...
|
||||
|
||||
def __int__(self):
|
||||
return self.value
|
||||
|
||||
__index__ = __int__ # Make indexing work
|
||||
```
|
||||
|
||||
|
||||
**Discussion**
|
||||
|
||||
Making a new primitive datatype is actually one of the most complicated
|
||||
programming tasks in Python. There are a lot of edge cases and low-level
|
||||
issues to worry about--especially with regard to how your type interacts
|
||||
with other Python types. Probably the key thing to keep in mind is that
|
||||
you can customize almost every aspect of how an object interacts with the
|
||||
rest of Python if you know the underlying protocols. If you're going to
|
||||
do this, it's advisable to look at the existing code for something similar
|
||||
to what you're trying to make.
|
||||
|
||||
\[ [Solution](soln2_4.md) | [Index](index.md) | [Exercise 2.3](ex2_3.md) | [Exercise 2.5](ex2_5.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