Introduction
In this lab, you will learn how to create a new primitive type in Python and implement essential methods for it. You'll also gain an understanding of Python's object protocol. In most Python programs, built - in primitive types like int, float, and str are used to represent data. However, Python enables you to create custom types, as seen in modules like decimal and fractions in the standard library.
In this lab, you will create a new primitive type named MutInt (Mutable Integer). Unlike Python's immutable integers, MutInt can be modified after creation. This exercise will showcase the fundamental principles required to create a fully functional primitive type in Python.
Creating a Basic MutInt Class
Let's start by creating a basic class for our Mutable Integer type. In programming, a class is like a blueprint for creating objects. In this step, we'll create the foundation of our new primitive type. A primitive type is a basic data type provided by a programming language, and here we're going to build our own custom one.
Open the WebIDE and navigate to the
/home/labex/projectdirectory. The WebIDE is an integrated development environment where you can write, edit, and run your code. Navigating to this directory ensures that all your files are organized in one place and can interact with each other properly.Open the
mutint.pyfile that was created for you in the setup step. This file will be the home for ourMutIntclass definition.Add the following code to define a basic
MutIntclass:
## mutint.py
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
The __slots__ attribute is used to define the attributes that this class can have. Attributes are like variables that belong to an object of the class. By using __slots__, we tell Python to use a more memory - efficient way to store attributes. In this case, our MutInt class will only have a single attribute called value. This means that each object of the MutInt class will only be able to hold one piece of data, which is the integer value.
The __init__ method is the constructor for our class. A constructor is a special method that gets called when an object of the class is created. It takes a value parameter and stores it in the value attribute of the instance. An instance is an individual object created from the class blueprint.
Let's test our class by creating a Python script to use it:
- Create a new file called
test_mutint.pyin the same directory:
## test_mutint.py
from mutint import MutInt
## Create a MutInt object
a = MutInt(3)
print(f"Created MutInt with value: {a.value}")
## Modify the value (demonstrating mutability)
a.value = 42
print(f"Modified value to: {a.value}")
## Try adding (this will fail)
try:
result = a + 10
print(f"Result of a + 10: {result}")
except TypeError as e:
print(f"Error when adding: {e}")
In this test script, we first import the MutInt class from the mutint.py file. Then we create an object of the MutInt class with an initial value of 3. We print the initial value, then modify it to 42 and print the new value. Finally, we try to add 10 to the MutInt object, which will result in an error because our class doesn't support the addition operation yet.
- Run the test script by executing the following command in the terminal:
python3 /home/labex/project/test_mutint.py
The terminal is a command - line interface where you can run various commands to interact with your system and your code. Running this command will execute the test_mutint.py script.
You should see output similar to this:
Created MutInt with value: 3
Modified value to: 42
Error when adding: unsupported operand type(s) for +: 'MutInt' and 'int'
Our MutInt class successfully stores and updates a value. However, it has several limitations:
- It doesn't display nicely when printed
- It doesn't support mathematical operations like addition
- It doesn't support comparisons
- It doesn't support type conversions
In the next steps, we'll address these limitations one by one to make our MutInt class behave more like a true primitive type.
Improving String Representation
When you print a MutInt object in Python, you'll see an output like <__main__.MutInt object at 0x...>. This output isn't very useful because it doesn't tell you the actual value of the MutInt object. To make it easier to understand what the object represents, we're going to implement special methods for string representation.
- Open
mutint.pyin the WebIDE and update it with the following code:
## mutint.py
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
def __str__(self):
"""Return a string representation for printing."""
return str(self.value)
def __repr__(self):
"""Return a developer-friendly string representation."""
return f'MutInt({self.value!r})'
def __format__(self, fmt):
"""Support string formatting with format specifications."""
return format(self.value, fmt)
We've added three important methods to the MutInt class:
__str__(): This method is called when you use thestr()function on the object or when you print the object directly. It should return a human-readable string.__repr__(): This method provides the "official" string representation of the object. It's mainly used for debugging and should ideally return a string that, if passed to theeval()function, would recreate the object.__format__(): This method allows you to use Python's string formatting system with yourMutIntobjects. You can use format specifications like padding and number formatting.
- Create a new test file called
test_string_repr.pyto test these new methods:
## test_string_repr.py
from mutint import MutInt
## Create a MutInt object
a = MutInt(3)
## Test string representation
print(f"str(a): {str(a)}")
print(f"repr(a): {repr(a)}")
## Test direct printing
print(f"Print a: {a}")
## Test string formatting
print(f"Formatted with padding: '{a:*^10}'")
print(f"Formatted as decimal: '{a:d}'")
## Test mutability
a.value = 42
print(f"After changing value, repr(a): {repr(a)}")
In this test file, we first import the MutInt class. Then we create a MutInt object with the value 3. We test the __str__() and __repr__() methods by using the str() and repr() functions. We also test direct printing, string formatting, and the mutability of the MutInt object.
- Run the test script:
python3 /home/labex/project/test_string_repr.py
When you run this command, Python will execute the test_string_repr.py script. You should see output similar to this:
str(a): 3
repr(a): MutInt(3)
Print a: 3
Formatted with padding: '****3*****'
Formatted as decimal: '3'
After changing value, repr(a): MutInt(42)
Now our MutInt objects display nicely. The string representation shows the underlying value, and we can use string formatting just like with regular integers.
The difference between __str__() and __repr__() is that __str__() is meant to produce a human-friendly output, while __repr__() should ideally produce a string that, when passed to eval(), would recreate the object. This is why we included the class name in the __repr__() method.
The __format__() method allows our object to work with Python's formatting system, so we can use format specifications like padding and number formatting.
Adding Mathematical Operations
Currently, our MutInt class doesn't support mathematical operations like addition. In Python, to enable such operations for a custom class, we need to implement special methods. These special methods are also known as "magic methods" or "dunder methods" because they are surrounded by double underscores. Let's add the functionality for addition by implementing the relevant special methods for arithmetic operations.
- Open
mutint.pyin the WebIDE and update it with the following code:
## mutint.py
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
def __str__(self):
"""Return a string representation for printing."""
return str(self.value)
def __repr__(self):
"""Return a developer-friendly string representation."""
return f'MutInt({self.value!r})'
def __format__(self, fmt):
"""Support string formatting with format specifications."""
return format(self.value, fmt)
def __add__(self, other):
"""Handle addition: self + other."""
if isinstance(other, MutInt):
return MutInt(self.value + other.value)
elif isinstance(other, int):
return MutInt(self.value + other)
else:
return NotImplemented
def __radd__(self, other):
"""Handle reversed addition: other + self."""
## For commutative operations like +, we can reuse __add__
return self.__add__(other)
def __iadd__(self, other):
"""Handle in-place addition: self += other."""
if isinstance(other, MutInt):
self.value += other.value
return self
elif isinstance(other, int):
self.value += other
return self
else:
return NotImplemented
We've added three new methods to the MutInt class:
__add__(): This method is called when the+operator is used with ourMutIntobject on the left side. Inside this method, we first check if theotheroperand is an instance ofMutIntor anint. If it is, we perform the addition and return a newMutIntobject with the result. If theotheroperand is something else, we returnNotImplemented. This tells Python to try other methods or raise aTypeError.__radd__(): This method is called when the+operator is used with ourMutIntobject on the right side. Since addition is a commutative operation (i.e.,a + bis the same asb + a), we can simply reuse the__add__method.__iadd__(): This method is called when the+=operator is used on ourMutIntobject. Instead of creating a new object, it modifies the existingMutIntobject and returns it.
- Create a new test file called
test_math_ops.pyto test these new methods:
## test_math_ops.py
from mutint import MutInt
## Create MutInt objects
a = MutInt(3)
b = MutInt(5)
## Test regular addition
c = a + b
print(f"a + b = {c}")
## Test addition with int
d = a + 10
print(f"a + 10 = {d}")
## Test reversed addition
e = 7 + a
print(f"7 + a = {e}")
## Test in-place addition
print(f"Before a += 5: a = {a}")
a += 5
print(f"After a += 5: a = {a}")
## Test in-place addition with reference sharing
f = a ## f and a point to the same object
print(f"Before a += 10: a = {a}, f = {f}")
a += 10
print(f"After a += 10: a = {a}, f = {f}")
## Test unsupported operation
try:
result = a + 3.5 ## Adding a float is not supported
print(f"a + 3.5 = {result}")
except TypeError as e:
print(f"Error when adding float: {e}")
In this test file, we first import the MutInt class. Then we create some MutInt objects and perform different types of addition operations. We also test the in-place addition and the case where an unsupported operation (adding a float) is attempted.
- Run the test script:
python3 /home/labex/project/test_math_ops.py
You should see output similar to this:
a + b = MutInt(8)
a + 10 = MutInt(13)
7 + a = MutInt(10)
Before a += 5: a = MutInt(3)
After a += 5: a = MutInt(8)
Before a += 10: a = MutInt(8), f = MutInt(8)
After a += 10: a = MutInt(18), f = MutInt(18)
Error when adding float: unsupported operand type(s) for +: 'MutInt' and 'float'
Now our MutInt class supports basic addition operations. Notice that when we used +=, both a and f were updated. This shows that a += 10 modified the existing object rather than creating a new one.
This behavior with mutable objects is similar to Python's built-in mutable types like lists. For example:
a = [1, 2, 3]
b = a
a += [4, 5] ## Both a and b are updated
In contrast, for immutable types like tuples, += creates a new object:
c = (1, 2, 3)
d = c
c += (4, 5) ## c is a new object, d still points to the old one
Implementing Comparison Operations
Currently, our MutInt objects cannot be compared with each other or with regular integers. In Python, comparison operations like ==, <, <=, >, >= are very useful when working with objects. They allow us to determine relationships between different objects, which is crucial in many programming scenarios such as sorting, filtering, and conditional statements. So, let's add comparison functionality to our MutInt class by implementing the special methods for comparison operations.
- Open
mutint.pyin the WebIDE and update it with the following code:
## mutint.py
from functools import total_ordering
@total_ordering
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
def __str__(self):
"""Return a string representation for printing."""
return str(self.value)
def __repr__(self):
"""Return a developer-friendly string representation."""
return f'MutInt({self.value!r})'
def __format__(self, fmt):
"""Support string formatting with format specifications."""
return format(self.value, fmt)
def __add__(self, other):
"""Handle addition: self + other."""
if isinstance(other, MutInt):
return MutInt(self.value + other.value)
elif isinstance(other, int):
return MutInt(self.value + other)
else:
return NotImplemented
def __radd__(self, other):
"""Handle reversed addition: other + self."""
return self.__add__(other)
def __iadd__(self, other):
"""Handle in-place addition: self += other."""
if isinstance(other, MutInt):
self.value += other.value
return self
elif isinstance(other, int):
self.value += other
return self
else:
return NotImplemented
def __eq__(self, other):
"""Handle equality comparison: 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):
"""Handle less-than comparison: self < other."""
if isinstance(other, MutInt):
return self.value < other.value
elif isinstance(other, int):
return self.value < other
else:
return NotImplemented
We've added several key improvements:
Import and use the
@total_orderingdecorator from thefunctoolsmodule. The@total_orderingdecorator is a powerful tool in Python. It helps us save a lot of time and effort when implementing comparison methods for a class. Instead of manually defining all six comparison methods (__eq__,__ne__,__lt__,__le__,__gt__,__ge__), we only need to define__eq__and one other comparison method (in our case,__lt__). The decorator will then automatically generate the remaining four comparison methods for us.Add the
__eq__()method to handle equality comparisons (==). This method is used to check if twoMutIntobjects or aMutIntobject and an integer have the same value.Add the
__lt__()method to handle less-than comparisons (<). This method is used to determine if oneMutIntobject or aMutIntobject compared to an integer has a smaller value.Create a new test file called
test_comparisons.pyto test these new methods:
## test_comparisons.py
from mutint import MutInt
## Create MutInt objects
a = MutInt(3)
b = MutInt(3)
c = MutInt(5)
## Test equality
print(f"a == b: {a == b}") ## Should be True (same value)
print(f"a == c: {a == c}") ## Should be False (different values)
print(f"a == 3: {a == 3}") ## Should be True (comparing with int)
print(f"a == 5: {a == 5}") ## Should be False (different values)
## Test less than
print(f"a < c: {a < c}") ## Should be True (3 < 5)
print(f"c < a: {c < a}") ## Should be False (5 is not < 3)
print(f"a < 4: {a < 4}") ## Should be True (3 < 4)
## Test other comparisons (provided by @total_ordering)
print(f"a <= b: {a <= b}") ## Should be True (3 <= 3)
print(f"a > c: {a > c}") ## Should be False (3 is not > 5)
print(f"c >= a: {c >= a}") ## Should be True (5 >= 3)
## Test with a different type
print(f"a == '3': {a == '3'}") ## Should be False (different types)
In this test file, we create several MutInt objects and perform different comparison operations on them. We also compare MutInt objects with regular integers and a different type (a string in this case). By running these tests, we can verify that our comparison methods work as expected.
- Run the test script:
python3 /home/labex/project/test_comparisons.py
You should see output similar to this:
a == b: True
a == c: False
a == 3: True
a == 5: False
a < c: True
c < a: False
a < 4: True
a <= b: True
a > c: False
c >= a: True
a == '3': False
Now our MutInt class supports all comparison operations.
The @total_ordering decorator is particularly useful because it saves us from having to implement all six comparison methods manually. By providing just __eq__ and __lt__, Python can derive the other four comparison methods automatically.
When implementing custom classes, it's generally a good practice to make them work with both objects of the same type and with built-in types where it makes sense. That's why our comparison methods handle both MutInt objects and regular integers. This way, our MutInt class can be used more flexibly in different programming scenarios.
Adding Type Conversions
Our MutInt class currently supports addition and comparison operations. However, it doesn't work with Python's built - in conversion functions such as int() and float(). These conversion functions are very useful in Python. For example, when you want to convert a value to an integer or a floating - point number for different calculations or operations, you rely on these functions. So, let's add the capabilities to our MutInt class to work with them.
- Open
mutint.pyin the WebIDE and update it with the following code:
## mutint.py
from functools import total_ordering
@total_ordering
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
def __str__(self):
"""Return a string representation for printing."""
return str(self.value)
def __repr__(self):
"""Return a developer - friendly string representation."""
return f'MutInt({self.value!r})'
def __format__(self, fmt):
"""Support string formatting with format specifications."""
return format(self.value, fmt)
def __add__(self, other):
"""Handle addition: self + other."""
if isinstance(other, MutInt):
return MutInt(self.value + other.value)
elif isinstance(other, int):
return MutInt(self.value + other)
else:
return NotImplemented
def __radd__(self, other):
"""Handle reversed addition: other + self."""
return self.__add__(other)
def __iadd__(self, other):
"""Handle in - place addition: self += other."""
if isinstance(other, MutInt):
self.value += other.value
return self
elif isinstance(other, int):
self.value += other
return self
else:
return NotImplemented
def __eq__(self, other):
"""Handle equality comparison: 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):
"""Handle less - than comparison: self < other."""
if isinstance(other, MutInt):
return self.value < other.value
elif isinstance(other, int):
return self.value < other
else:
return NotImplemented
def __int__(self):
"""Convert to int."""
return self.value
def __float__(self):
"""Convert to float."""
return float(self.value)
__index__ = __int__ ## Support array indexing and other operations requiring an index
def __lshift__(self, other):
"""Handle left shift: self << other."""
if isinstance(other, MutInt):
return MutInt(self.value << other.value)
elif isinstance(other, int):
return MutInt(self.value << other)
else:
return NotImplemented
def __rlshift__(self, other):
"""Handle reversed left shift: other << self."""
if isinstance(other, int):
return MutInt(other << self.value)
else:
return NotImplemented
We've added three new methods to the MutInt class:
__int__(): This method is called when you use theint()function on an object of ourMutIntclass. For example, if you have aMutIntobjecta, and you writeint(a), Python will call the__int__()method of theaobject.__float__(): Similarly, this method is called when you use thefloat()function on ourMutIntobject.__index__(): This method is used for operations that specifically require an integer index. For instance, when you want to access an element in a list using an index, or perform bit - length operations, Python needs an integer index.__lshift__(): This method handles left shift operations when theMutIntobject is on the left side of the<<operator.__rlshift__(): This method handles left shift operations when theMutIntobject is on the right side of the<<operator.
The __index__ method is crucial for operations that demand an integer index, like list indexing, slicing, and bit - length operations. In our simple implementation, we set it to be the same as __int__ because our MutInt object's value can be directly used as an integer index.
The __lshift__ and __rlshift__ methods are essential for supporting bitwise left shift operations. They allow our MutInt objects to participate in bitwise operations, which is a common requirement for integer-like types.
- Create a new test file called
test_conversions.pyto test these new methods:
## test_conversions.py
from mutint import MutInt
## Create a MutInt object
a = MutInt(3)
## Test conversions
print(f"int(a): {int(a)}")
print(f"float(a): {float(a)}")
## Test using as an index
names = ['Dave', 'Guido', 'Paula', 'Thomas', 'Lewis']
print(f"names[a]: {names[a]}")
## Test using in bit operations (requires __index__)
print(f"1 << a: {1 << a}") ## Shift left by 3
## Test hex/oct/bin functions (requires __index__)
print(f"hex(a): {hex(a)}")
print(f"oct(a): {oct(a)}")
print(f"bin(a): {bin(a)}")
## Modify and test again
a.value = 4
print(f"\nAfter changing value to 4:")
print(f"int(a): {int(a)}")
print(f"names[a]: {names[a]}")
- Run the test script:
python3 /home/labex/project/test_conversions.py
You should see output similar to this:
int(a): 3
float(a): 3.0
names[a]: Thomas
1 << a: 8
hex(a): 0x3
oct(a): 0o3
bin(a): 0b11
After changing value to 4:
int(a): 4
names[a]: Lewis
Now our MutInt class can be converted to standard Python types and used in operations that require an integer index.
The __index__ method is particularly important. It was introduced in Python to allow objects to be used in situations where an integer index is required, such as list indexing, bitwise operations, and various functions like hex(), oct(), and bin().
With these additions, our MutInt class is now a fairly complete primitive type. It can be used in most contexts where a regular integer would be used, with the added benefit of being mutable.
Complete MutInt Implementation
Here's our complete MutInt implementation with all the features we've added:
## mutint.py
from functools import total_ordering
@total_ordering
class MutInt:
"""
A mutable integer class that allows its value to be modified after creation.
"""
__slots__ = ['value']
def __init__(self, value):
"""Initialize with an integer value."""
self.value = value
def __str__(self):
"""Return a string representation for printing."""
return str(self.value)
def __repr__(self):
"""Return a developer - friendly string representation."""
return f'MutInt({self.value!r})'
def __format__(self, fmt):
"""Support string formatting with format specifications."""
return format(self.value, fmt)
def __add__(self, other):
"""Handle addition: self + other."""
if isinstance(other, MutInt):
return MutInt(self.value + other.value)
elif isinstance(other, int):
return MutInt(self.value + other)
else:
return NotImplemented
def __radd__(self, other):
"""Handle reversed addition: other + self."""
return self.__add__(other)
def __iadd__(self, other):
"""Handle in - place addition: self += other."""
if isinstance(other, MutInt):
self.value += other.value
return self
elif isinstance(other, int):
self.value += other
return self
else:
return NotImplemented
def __eq__(self, other):
"""Handle equality comparison: 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):
"""Handle less - than comparison: self < other."""
if isinstance(other, MutInt):
return self.value < other.value
elif isinstance(other, int):
return self.value < other
else:
return NotImplemented
def __int__(self):
"""Convert to int."""
return self.value
def __float__(self):
"""Convert to float."""
return float(self.value)
__index__ = __int__ ## Support array indexing and other operations requiring an index
def __lshift__(self, other):
"""Handle left shift: self << other."""
if isinstance(other, MutInt):
return MutInt(self.value << other.value)
elif isinstance(other, int):
return MutInt(self.value << other)
else:
return NotImplemented
def __rlshift__(self, other):
"""Handle reversed left shift: other << self."""
if isinstance(other, int):
return MutInt(other << self.value)
else:
return NotImplemented
This implementation covers the key aspects of creating a new primitive type in Python. To make it even more complete, you could implement additional methods for other operations like subtraction, multiplication, division, etc.
Summary
In this lab, you have learned how to create your own primitive type in Python. Specifically, you've mastered creating a mutable integer class similar to built - in types, implementing special methods for object display, adding support for mathematical and comparison operations, and enabling type conversions for various Python contexts.
These concepts are essential for understanding Python's object model and can be used to create custom types that integrate well with built - in operations. To further your knowledge, consider implementing more mathematical operations, adding support for other built - in functions, and exploring complex types like custom collections. Custom types in Python are a powerful tool to extend the language for specific needs.