3
from fractions import Fraction


class F1(Fraction):
    def __init__(self, *args, **kwargs):
        Fraction.__init__(*args, **kwargs)

class F2(Fraction):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


Fraction(1, 10) # Fraction(1, 10)

F1(1, 10) # F1(1, 10)

F2(1, 10) # TypeError: object.__init__() takes exactly one argument (the instance to initialize)

How does this happen? Could someone elaborate a bit on the super function?

Python version: 3.8.10

  • 1
    I don't know the full answer, but `Fraction` is an immutable that is created with `__new__` and not initialized with `__init__`. To subclass you need to play the `__new__` game and typically not implement `__init__`. See `Fraction.py` and what it does. I'd guess that _both_ F1 and F2 examples should fail. I can't explain why F1 works. – tdelaney Jul 29 '22 at 18:18

1 Answers1

2

TLDR; Fraction uses __new__ instead of __init__.

Python objects are assembled in two steps. First, they are created with __new__ and then they are initialized with __init__. Usually we use the default object.__new__ to create a new empty object and then override __init__ for anything special that needs to be done.

But some classes want to control the object creation step by implementing their own __new__. This is especially the case for immutable classes like Fraction. Fraction.__new__ returns an object of its own super class and when that happens, python skips calling __init__ completely. In the case of Fraction, its __init__ is really just object.__init__ which only accepts the single self parameter and just returns without doing anything. It is never meant to be called.

When you implemented F1.__init__, you had a bug and this bug masks the problem. When you call a superclass method directly, you need to put the self parameter in the call. You should have done Fraction.__init__(self, *args, **kwargs). Had you done so, you'd get the same error as in the F2 case (because super().__init__(*args, **kwargs) does add the self).

But really you have a more pressing problem. Its okay to have your own __init__, but with restrictions. You can't initialize the Fraction because that was done in __new__ before __init__ was called. And you can't call Fraction.__init__ which does nothing except explode when given parameters. You could add other attributes to the object, but that's about it. But other strange things will happen. Unless you override methods like __add__, they will return objects of the original Fraction type because that's the __new__ that is being called. When your parent class uses __new__, you really want to override that __new__.

tdelaney
  • 73,364
  • 6
  • 83
  • 116
  • 2
    An important point: The *reason* `Fraction` uses `__new__` is because all logically immutable objects should use `__new__`, not `__init__`, and all the immutable types that ship with Python obey this rule (e.g. `int`, `float`, `str`, `tuple`; if you subclass them, you need to override `__new__`, not `__init__`). The reason being that `__init__` can be called again and change the state (there are some weird nooks in the language that do this on purpose), while `__new__` (which does not receive an existing instance) can't mutate an existing instance. – ShadowRanger Jul 29 '22 at 19:36
  • @tdelaney any comment on the F1.__init__() bug ? had to ask coz i went on a langauge data model safari for getting this answer, and i still cant figure the bug – Cpreet Jul 29 '22 at 19:39
  • @ShadowRanger - Thanks for fleshing that out, – tdelaney Jul 29 '22 at 20:04
  • @Cpreet - The basic problem is that `Fraction.__init__` is an invalid call. Had it been written as `Fraction.__init__(self, *args, **kwargs)` (you need to supply the `self` yourself in this case), you'd see the same error as the `F2` case. `Fraction.__init__` is really just `object.__init__`, which does nothing. As written, python passes the `arg` tuple as the first parameter and since `object.__init__` does nothing, the fact this its the arg tuple and not `self` is not discovered. – tdelaney Jul 29 '22 at 20:08
  • 1
    @Cpreet - I was surprised by this. When making the call, python pushes args as a tuple and kwargs as a dict on the stack and then calls the function. The function call step expands these into the parameter list of the function. Why this expansion worked when `object.__init__` doesn't have extra parameters is still puzzling to me. – tdelaney Jul 29 '22 at 20:17
  • 2
    Exactly, @tdelaney, i was able to puzzle together that the mro goes through the tree and calls object init, but rn with the 2 agruments it still works as a normal Fraction instance. Is it a big enough to be submitted to the python git ? Nicely explained tho – Cpreet Jul 30 '22 at 07:06