Subclassing Immutable Built-in Types
If you’re interested in converting units, then you can check out Pint.
00:00
Subclassing Immutable Built-in Types. Let’s start with a use case of .__new__()
that consists of subclassing an immutable built-in type. As an example, let’s say you need to write a Distance
class as a subclass of Python’s float
type.
00:17
Your class will have an additional attribute to store the unit that’s used to measure the distance. Here’s a first approach to this problem, using the .__init__()
method.
00:45
When you subclass an immutable built-in data type, you get an error. Part of the problem is that the value is set during creation, and it’s too late to change it during initialization. Additionally, float.__new__()
is called under the hood, and it doesn’t deal with extra arguments in the same way as object.__new()
. This is what raises the error seen in this example.
01:08
To work around this issue, you can initialize the object at creation time with .__new__()
instead of overriding .__init__()
. Here’s how this is done in practice.
01:24
Here, .__new__()
runs the three steps that you learned in the previous section. First, the method creates a new of the current class, cls
, by calling super().__new__()
.
01:35
This time, the call rolls back to float.__new__()
, which creates a new instance and initializes it using value
as an argument.
01:43
Then the method customizes the new instance by adding a .unit
attribute to it. Finally, the new instance gets returned.
01:53
Now your Distance
class works as expected, allowing you to use an instance attribute for storing the unit in which you’re measuring the distance.
02:01
Unlike the floating-point value stored in a given instance of Distance
, the .unit
attribute is mutable, so you can change its value any time you like.
02:10
Finally, note how a call to the dir()
function reveals that your class inherits features and methods from float
.
02:22
Note that the Distance
class in this example doesn’t provide a proper unit conversion mechanism. This means that something like Distance(10, "km") +
Distance(20, "miles")
won’t attempt at converting units before adding the values.
02:39 If you’re interested in converting units, then check out this project on PyPI. Having seen how to subclass a built-in, in the next section of the course, you’ll see how a class can return an instance of a different class.
praghavan1973 on June 13, 2023
In response to question from raaki88:
I think the new for float accepts only one argument which is the value for float. For the new subclass we are creating we can accept depending on our definition of new.
mochibeepboop on Sept. 18, 2023
Why do we need to overrride new just to create a simple class that calculates distance?
Bartosz Zaczyński RP Team on Sept. 18, 2023
@mochibeepboop Strictly speaking, you don’t have to override the .__new__()
method to implement a class that calculates distance. For example, you could’ve implemented the same logic as a data class, avoiding inheritance and many challenges associated with it:
>>> from dataclasses import dataclass
>>> @dataclass
... class Distance:
... value: float
... unit: str
...
... def __add__(self, other):
... if isinstance(other, Distance):
... if self.unit == other.unit:
... return Distance(self.value + other.value, self.unit)
... raise NotImplementedError
...
>>> d1 = Distance(42.0, "miles")
>>> d2 = Distance(8.0, "miles")
>>> d1 + d2
Distance(value=50.0, unit='miles')
Darren’s code serves as an example to demonstrate how you can subclass a built-in immutable data type, such as float
. Notice that floating-point numbers take one argument—the value—and you want your Distance
class to take an additional unit argument. When you override .__init__()
by adding this extra argument, instantiating the class results in an error:
>>> class Distance(float):
... def __init__(self, value, unit):
... super().__init__(value)
... self.unit = unit
...
>>> Distance(42.0, "miles")
Traceback (most recent call last):
File "<input>", line 1, in <module>
Distance(42.0, "miles")
TypeError: float expected at most 1 argument, got 2
That’s because super().__init__()
still expects only the value
argument. Therefore, you must override the .__new__()
method in such a way that it accepts both the value
and unit
arguments but only passes the value to super().__new__()
:
>>> class Distance(float):
... def __new__(cls, value, unit):
... instance = super().__new__(cls, value)
... instance.unit = unit
... return instance
...
>>> Distance(42.0, "miles")
42.0
>>> distance = Distance(42.0, "miles")
>>> distance.unit
'miles'
Become a Member to join the conversation.
raaki88 on Sept. 29, 2022
Hi Darren,
Thank you for the course, one question , you did say that new will accept only one argument for class, how come we can pass a value as well in this case ?