A Callable Float? Fun and Creativity in Python

Among the implicit information types in Python, we have various sorts addressing numbers, the most significant being int and float. As everything in Python, their cases are objects; and as articles, they have their own traits and strategies. For instance, this is the very thing examples of the float type offer:


As you see, float numbers offer a wide range of techniques to utilize. What they don't offer is a .__cal__() technique, and that implies you can't call them.


Have you at any point considered the reason why we can't call a drifting point number the manner in which you can call a capability? Look:

>>> i = 10.0012
>>> i()
Traceback (most recent call last):
...
TypeError: 'float' object is not callable

It doesn't work. Also, for what reason does it not work?!


Frankly… I've never contemplated why floats are not callable — however it sounds good to me. How could they? What for? Do you see a specific justification for them to be? I don't really.

However, this doesn't mean we won't execute such usefulness — we most certainly will. Also, how could we believe should do that? For three reasons: to learn Python, to be innovative, and to master being inventive.

Figuring out how to be innovative is a significant part of learning a programming language. At some point, in a Python project, you will end up in a tough spot, in which standard arrangements don't work; you should think and be imaginative. Your imagination might assist you with sorting out an inventive, abnormal arrangement, one that will assist you with taking care of this weird or abnormal issue.

Subsequently, in this article, we will execute a callable float type; this is most certainly not an ordinary situation. We will continue in advances, and I will make sense of each move toward detail. Attempt to imaginative when perused on. Maybe being so will assist you with concocting your own thoughts regarding how to work on the arrangement. Assuming this is the case, execute them, and kindly offer them in the remarks.


Implementation

We will begin by carrying out the accompanying basic methodology: in the event that a client calls a float, it restores the number adjusted to three decimal digits. For the occasion, we should simplify it — I will hardcode adjusting to three decimal digits; We will change that later.

# callable_float.py
class Float(float):
def __call__(self):
return round(self, 3)

What's more, that is all we really want! As you can see underneath, in typical use cases, an example of Float acts very much like a customary float object:

>>> from callable_float import Float
>>> i = Float(10.0012)
>>> i * 2
20.0024
>>> i - 1
9.0012
>>> round(2 * (i + 1) / 7, 5)
3.14320

In contrast to floats, in any case, Floats are callable:

>>> i()
10.001

Presto — a callable drifting point number.

Don't bother hardcode the quantity of decimal digits to be utilized in adjusting, notwithstanding. We can empower the client to choose the number of decimal digits to utilize. To do as such, enough to add a digits contention to the .__call__() technique:

# callable_float.py
class Float(float):
def __call__(self, digits=3):
return round(self, digits)

>>> i(1)
10.0
>>> i(3)
10.001
>>> i(10)
10.0012


Perfect! Note that this is exactly what we would get by rounding the corresponding float number:

>>> j = float(i)
>>> i(1) == round(j, 1)
True
>>> i(3) == round(j, 3)
True
>>> i(10) == round(j, 10)
True

We may want to return a Float object instead of a float:

# callable_float.py
class Float(float):
def __call__(self, digits=3):
return Float(round(self, digits))

Here, the only thing that Float numbers can do when being called is rounding. Boring! Let it be able to do anything.

To implement such a general concept, we need to make the .__call__() method a higher-order method, which here means that it takes a function (actually, a callable) as an argument. The below class implements this functionality:

# callable_float.py
from typing import Callable

class Float(float):
def __call__(self, func: Callable):
return func(self)

Note that this time, we did not change the type of the returned value to Float, as the user may wish to use a func() function that returns an object of another type.

This is how this version of Float works:

>>> from callable_float import Float
>>> i = Float(12.105)
>>> 2*i
24.21
>>> i(round)
12
>>> i(lambda x: 200)
200
>>> i(lambda x: x + 1)
13.105
>>> def square_root_of(x):
... return x**.5
>>> i(square_root_of)
3.479224051422961
>>> i(lambda x: round(square_root_of(x), 5))
3.47922
>>> i = Float(12345.12345)
>>> i(lambda x: Float(str(i)[::-1]))
54321.54321

It works as expected, but it definitely has a significant drawback: you cannot use additional arguments to the func() function. We can easily implement this functionality, thanks to the *args and **kwargs arguments, enabling the user to provide any arguments:

# callable_float.py
from typing import Callable

class Float(float):
def __call__(self, func: Callable, *args, **kwargs):
return func(self, *args, **kwargs)
>>> i(lambda x, digits: round(x**.5, digits), 5)
111.10861
>>> def rounded_square_root_of(x, digits=3):
... return round(x**.5, digits)
>>> i(rounded_square_root_of)
111.109
>>> i(rounded_square_root_of, 5)
111.10861
>>> j = float(i)
>>> i(rounded_square_root_of, 5) == rounded_square_root_of(j, 5)
True

All works just fine and dandy. Consider the following notes:

  • The user provides a function to be applied for a class’s instance. It can take no arguments or any number of them, both positional (*args) and keyword (**kwargs). Then calling the Float value as a function and a function func() as an argument to this function is the same as calling this function with the value as its argument… If that’s not crazy, then what is?!
  • This time the return value of Float.__call__() can be of any type, and it is the same type that the func() function’s return value has.


Buy me a coffee

Back to top