Reputation: 920
I wrote a mixed number fraction class to extend and expand the functionality of the standard library Fraction
class in order to accept anything that Fraction
would and more: Mixed('3 4/5')
== Mixed(3,4,5)
== Mixed(Fraction(19,5))
== Fraction(19,5)
, etc. I've read a lot about super
and I'm still not 100% sure I understand the what and why of this line in the Fraction
class source __new__
definition:
self = super(Fraction, cls).__new__(cls)
I suspect that it makes every reference to self
point to and create a new instance of Fraction
due to the class being immutable. Is that what is happening and why?
Upvotes: 3
Views: 180
Reputation: 1122082
super(Fraction, cls).__new__
will look through the class MRO to look for the next .__new__
method, starting the search one step on from the location of Fraction
:
>>> from fractions import Fraction
>>> Fraction.mro()
[<class 'fractions.Fraction'>, <class 'numbers.Rational'>, <class 'numbers.Real'>, <class 'numbers.Complex'>, <class 'numbers.Number'>, <class 'object'>]
So it'll look at all the other classes to see where the next __new__
is defined, the code then calls that. Ultimately, that'll be object.__new__
, where Python's C code creates the actual C structures that form the number object:
>>> for cls in Fraction.mro()[1:]:
... if '__new__' in cls.__dict__:
... print(cls)
...
<class 'object'>
The stated purpose of the __new__
method is to create a new instance. And because numbers are indeed immutable, that is the point where you want to hook in to be able to customize how the instance is created, because once it exists, it cannot be altered anymore.
The name self
is just a local name. It matches the convention used by methods, but __new__
is not an ordinary bound method as there is nothing to bind to when creating a new instance. You can replace that name throughout the function with something entirely different (instance
, this_new_object_we_just_created
, etc.) and the code would still work the same. self
in other functions is not affected by it.
As it happens, Fraction
instances are mutable; the class defines _numerator
and _denominator
slots, which can still be rebound after the instance has been created. The Fraction.__new__()
factory method actually does this; it assigns new values to those attributes. After adjusting these attributes, self
is returned, thus fulfilling the contract of the __new__
method, to return the new instance.
In principle, setting the _numerator
and _denominator
attributes could all have been done in an __init__
method too. The Python developers have however decided to stick to the convention for immutable types as the class is meant to be treated as immutable:
>>> fraction = Fraction(3, 4)
>>> fraction.numerator
3
>>> fraction.numerator = 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> fraction._numerator = 4
>>> fraction.numerator
4
but as you can see, if you know about the mutable attributes, you can still mutate the instance from the outside.
Upvotes: 3