Python: Can a subclass of float take extra arguments in its constructor?

Viewed 6885

In Python 3.4, I'd like to create a subclass of float -- something that can be used in math and boolean operations like a float, but has other custom functionality and can receive an argument at initialization that controls that functionality. (Specifically, I wanted to have a custom __str__ and a parameter that is used in that method.)

However, I can't seem to get a subclass of float to have a functional two-argument constructor. Why? Is this simply a limitation on extending built-in types?

Example:

class Foo(float):
    def __init__(self, value, extra):
        super().__init__(value)
        self.extra = extra

Now if I try Foo(1,2) I get:

TypeError: float() takes at most 1 argument (2 given)

Surprisingly, my new __init__'s arguments are enforced too, so if I do Foo(1) I get:

TypeError: __init__() missing 1 required positional argument: 'extra'

What's the deal here? I've done similar things with subtypes of list and was surprised it didn't work on float.

5 Answers

Both @cgogolin and @qvpham provide working answers. However, I reckon that float.__init__(value) within the __init__ method is irrelevant to the initialization of Foo. That is, it does nothing to initialize attributes of Foo. As such, it rather causes confusion on the necessity of the operation toward subclassing the float type.

Indeed, the solution can be further simplified as follows:

In [1]: class Foo(float):
   ...:     def __new__(cls, value, extra):
   ...:        return super().__new__(cls, value)
   ...:     def __init__(self, value, extra):
   ...:        self.extra = extra

In [2]: foo = Foo(1,2)
   ...: print(str(foo))
1.0

In [3]: print(foo.extra)
2

While you can handle initialization in the __new__ method, because it's always called before __init__ (or even instead of, if __new__'s returned object is other than an instance of the class), it's best practice decoupling object initizalization in __init__ and leaving __new__ only for object creation.

For instance, in that way you would be able to subclass Foo. (Furthermore, passing *args, **kwargs to __new__ will allow the subclass have any number of positional or named arguments.)

class Foo(float):
    def __new__(cls, value, *args, **kwargs):
        return super().__new__(cls, value)

    def __init__(self, value, extra):
        self.extra = extra

class SubFoo(Foo):
    def __init__(self, value, extra, more):
        super().__init__(value, extra)
        self.more = more

However, if you handle initialization in __new__ you will inherit object's __init__ which hasn't more arguments than the instance itself. And you won't be able to subclass it by the common way.

class Bar(float):
    def __new__(cls, value, extra):
        self = super().__new__(cls, value)
        self.extra = extra
        return self

class SubBar(Bar):
    def __init__(self, value, extra):
        super().__init__(value, extra)
>>> sub_bar = SubBar(1, 2)
TypeError: object.__init__() takes no parameters

You can do this without implementing __init__ at all:

class Foo(float):
    def __new__(cls, value, extra):
        instance = super().__new__(cls, value)
        instance.extra = extra
        return instance

In use:

>>> foo = Foo(1, 2)
>>> print(foo)
1.0
>>> print(foo.extra)
2
Related