24

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.

Shimon Rura
  • 439
  • 4
  • 13
  • 3
    Related: http://stackoverflow.com/questions/1936457/sub-classing-float-type-in-python-fails-to-catch-exception-in-init – Robᵩ Mar 11 '16 at 15:29
  • 2
    Not an answer to your question, but a solution to your problem: http://ideone.com/nIPim9 – Robᵩ Mar 11 '16 at 15:36
  • Interesting, @Robᵩ. Seems like overriding `__new__` might be the way to do what I want, but not exactly sure how to make that work yet. – Shimon Rura Mar 11 '16 at 15:40
  • @Robᵩ perfect, that approach is what I was looking for. TIL about the difference between constructors and initializers in Python. However, I'm still trying to grasp some of this. Like in your example, we construct a `float` in `__new__` but assign its attribute `extra` in `__init__`, which you couldn't normally do to a `float` instance. – Shimon Rura Mar 11 '16 at 15:49

5 Answers5

27

As float is immutable you have to overwrite __new__ as well. The following should do what you want:

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

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

See also Sub-classing float type in Python, fails to catch exception in __init__()

Community
  • 1
  • 1
cgogolin
  • 960
  • 1
  • 10
  • 22
  • Yes, this is a working approach - thanks! I'm still puzzled by why this is necessary, though. For your `__init__` method, `self` is a `float` not a `Foo`, right? Because we created it via `float.__new__`? – Shimon Rura Mar 11 '16 at 15:55
  • Actually, http://stackoverflow.com/a/13534717/204131 points out that `float.__new__` is passed a parameter of `Foo`, so it should create an instance of `Foo` rather than of float. – Shimon Rura Mar 11 '16 at 15:56
11

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
kodemartin
  • 440
  • 6
  • 9
  • Note: You don't need to cast foo to a str since its __str()__ method is still fine as it would be for a normal float. – Gigi Bayte 2 Oct 14 '19 at 06:00
3

the solution of cgogolin is right. it's like so with another immutable classes like int, str, ... But i will write:

class Foo(float):
    def __new__(cls, value, extra):
       return super().__new__(cls, value)
    def __init__(self, value, extra):
       float.__init__(value)
       self.extra = extra
qvpham
  • 1,896
  • 9
  • 17
2

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
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
1

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
Nuno André
  • 4,739
  • 1
  • 33
  • 46
  • The value of Foo is never initialized in this solution (if one could simply set it in `__init__` then there would not be any need to use `__new__` to begin width). – Rene Pöschl Apr 16 '21 at 17:46
  • Fixed, good catch. That's exactly the problem that I tried to show. `float` is a C primitive type, so it doesn't make sense for it to accept variadic arguments, but doing this when subclassing avoids having to redefine `__new__` for subsequent subclassing. – Nuno André Apr 17 '21 at 02:46