JB0x2D1
JB0x2D1

Reputation: 920

aliasing self = super(...).__new__(...)? why?

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

Answers (1)

Martijn Pieters
Martijn Pieters

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

Related Questions