Techno Blender
Digitally Yours.

rounder: Rounding Numbers in Complex Python Objects | by Marcin Kozak | Oct, 2022

0 57


The rounder package enables you to round all numbers in any object, with just one command

Rows of floating-point numbers after rounding to two decimal digits.
rounder: Rounding numbers in Python has never been easier. Photo by Mika Baumeister on Unsplash

Rounding numbers seldom causes problems. In the standard library, we have function round(), math.ceil() and math.floor(), thanks to which we can use three standard types of rounding. This should suffice, shouldn’t it?

Not always, actually. Imagine you have the following object that you want to round to two decimal digits:

Note: In this example, it does not matter what the object and its particular fields mean. It’s just the representation of a possible complex nested object. For the sake of simplicity, however, I did not make it too complex. When working with it, however, imagine a complex nested dictionary, with its fields being other nested dictionaries, lists and objects of yet other types.

How would you do it? How would you approach rounding all numbers inside such an object? You may think it’s relatively easy, as it’s enough to write a dedicated function that will work with this particular type of a nested object. This will work, but what if the object changes a little bit? You would have to update the function, in order to make it reflect the new structure of the object. Or, you could write a recursive function to do that, but the function would not have to be that easy to write to write, as it would need to handle so many different types that Python offers.

So, from the simplicity of round(1.122, 1), we moved to a function that hardcodes rounding an object of a specific type and structure, or to rather difficult recursive function. This has ceased being simple…

Is this the only way? Isn’t there a simpler way? Does not Python promises to be simple?

Fortunately, there is an easy way: the rounder package. It offers functions that allow for rounding numbers in complex and nested objects of many various types.

Let’s return to our example. We wanted to round the numbers from the obj object to two decimal digits. This is how you can do it using rounder:

Isn’t this simple?

Now, imagine you have decided to round numbers in obj to five significant digits. Python does not offer you a built-in function for that — but rounder does. You can use rounder.signif() for a number, and rounder.signif_object() for nested objects:

Note: obj is a mutable object, and since we rounded it before, it changed. Hence to use its original version again, you need to recreate it, in the same way we created it above.

In a similar way, you can also use rounder.ceil_object() and rounder.floor_object(), which correspond to math.ceil() and math.floor() functions, respectively.

Mutable versus immutable objects

We must remember that mutable objects, unlike immutable ones, are changed in place. This means that when you update it with or without assigning it to a new name, the original object will be changed. The same way, if you change its (shallow) copy, the original object will be updated.

The same rules apply for rounder operations. Remember that it’s not typical of rounder but of Python. In other words, rounder implements Python’s typical way of handling mutable and immutable objects.

rounder functions, however, make working with mutable objects simpler. For the sake of simplicity, this time we will work with simpler and smaller objects:

We did expect this, but since lists are mutable, x has been changed, too:

If you do not want this to happen, rounder can help you. You can use argument use_copy, whose default is False; that’s why on default, rounder changes the original object. This is the default behavior of Python: If you want to create a copy of an object, you have to do it manually.

Note: It’s a good place to mention shallow and deep copies. You can read about this topic in the official documentation of the standard-library copy module or here. If you need or want to deepen or refresh your knowledge about it, you can make a break and do it now.

This is how use_copy changes the behavior of rounder functions:

We can use the other rounder functions with use_copy, too:

As for rounding to significant digits, remember that this operation affects also integers:

Custom rounding

The rounder package offers also a general function, rounder.map_object(), which enables you to apply any callable that takes a number and returns a number (so, Callable[numbers.Number, numbers.Number]). You can use it to apply custom rounding.

For example, imagine you want to apply the following rounding of a floating-point number, say x:

  • if x is an integer, return it untouched
  • round to integers when abs(x) > 100
  • round to 1 decimal digit when 100 > abs(x) > 10
  • round to 2 decimal digits when 10 > abs(x) > 1
  • otherwise, round to 3 decimal digits

Such a rounding method can be used, for instance, when you round an object with numbers with different units or at different scales. The following function does this:

The function’s name, round_float, means that it will round only floats. It could be named round_only_floats, but this name would not fully represent what it does; this is because it does not round non-numbers. So, I named it round_float, to differentiate it from round.

Let’s see this in action:

Now, let’s use this function to the obj object we used above:

As you see, the function’s signature is similar to that of the map() function, the first argument being a callable to be used for an object provided as the second argument. This can help you remember how to use rounder.map_object().

Actually, you can use this function to apply any callable Callable[numbers.Number, numbers.Number] to all numbers in the object, even those having nothing to do with rounding. Consider this example:

Types

It’s worth adding that rounder‘s functions work with many various types, such as all the built-in container types (like dict, list, tuple and set), but also generators, map and filter objects, queues, named tuples, array.array, and more. You will find their list in the package’s repository. Note that when you feed in a generator type into a rounder function, you will receive a generator, too.

Exception handling

Typical rounding functions throw TypeError when you use them for non-numbers:

The rounder.signif() function behaves in a similar way:

It is not so with the rounder functions whose names end with _object:

These functions behave like this because when the object is a container of objects, the function is run recursively on them, rounding (or not, if it’s not a number) every lowest-level element of the object.

Implementation

Advanced developers may appreciate the way that rounder was developed. The idea came from Ruud van der Ham, the package’s co-author. The essence of the package is implemented using a closure, an elegant approach — much more elegant than the original class-based approach, used in the first version of the package (not released).

You can learn more about the package’s implementation from the package repository, and in particular, from its main module, rounder.

The rounder package makes rounding easy. With it, you can use the most common rounding methods:

  • rounding a number to a particular number of decimal digits (round_object());
  • rounding a number down (floor_object()) or up (ceil_object()) to the nearest integer;
  • rounding a number to a particular number of significant digits (signif() and signif_object());

but also a custom approach to rounding, thanks to

  • rounding a number using a custom function (map_object()).

But this is not what makes rounder so useful. It is so useful because you can use therounder functions whose names end with _object for any complex and/or nested object type, and the function will round all the numbers inside the object.

The package also makes it simple to work with mutable objects. Normally, when you want to create a copy of a mutable object, you need to call copy.deepcopy(), which creates a deep copy of the object. With rounder, you do not have to do it; enough to use use_copy=True argument, and the function will do it for you.

In short, whenever you need to round numbers in more complex objects than a number (so, an object following the numbers.Number abstract base class), you can find rounder particularly helpful. Its functions will also work with regular numbers. With the appearance of the rounder package, the complexity of an object in which we want to round all numbers ceased being a problem.


The rounder package enables you to round all numbers in any object, with just one command

Rows of floating-point numbers after rounding to two decimal digits.
rounder: Rounding numbers in Python has never been easier. Photo by Mika Baumeister on Unsplash

Rounding numbers seldom causes problems. In the standard library, we have function round(), math.ceil() and math.floor(), thanks to which we can use three standard types of rounding. This should suffice, shouldn’t it?

Not always, actually. Imagine you have the following object that you want to round to two decimal digits:

Note: In this example, it does not matter what the object and its particular fields mean. It’s just the representation of a possible complex nested object. For the sake of simplicity, however, I did not make it too complex. When working with it, however, imagine a complex nested dictionary, with its fields being other nested dictionaries, lists and objects of yet other types.

How would you do it? How would you approach rounding all numbers inside such an object? You may think it’s relatively easy, as it’s enough to write a dedicated function that will work with this particular type of a nested object. This will work, but what if the object changes a little bit? You would have to update the function, in order to make it reflect the new structure of the object. Or, you could write a recursive function to do that, but the function would not have to be that easy to write to write, as it would need to handle so many different types that Python offers.

So, from the simplicity of round(1.122, 1), we moved to a function that hardcodes rounding an object of a specific type and structure, or to rather difficult recursive function. This has ceased being simple…

Is this the only way? Isn’t there a simpler way? Does not Python promises to be simple?

Fortunately, there is an easy way: the rounder package. It offers functions that allow for rounding numbers in complex and nested objects of many various types.

Let’s return to our example. We wanted to round the numbers from the obj object to two decimal digits. This is how you can do it using rounder:

Isn’t this simple?

Now, imagine you have decided to round numbers in obj to five significant digits. Python does not offer you a built-in function for that — but rounder does. You can use rounder.signif() for a number, and rounder.signif_object() for nested objects:

Note: obj is a mutable object, and since we rounded it before, it changed. Hence to use its original version again, you need to recreate it, in the same way we created it above.

In a similar way, you can also use rounder.ceil_object() and rounder.floor_object(), which correspond to math.ceil() and math.floor() functions, respectively.

Mutable versus immutable objects

We must remember that mutable objects, unlike immutable ones, are changed in place. This means that when you update it with or without assigning it to a new name, the original object will be changed. The same way, if you change its (shallow) copy, the original object will be updated.

The same rules apply for rounder operations. Remember that it’s not typical of rounder but of Python. In other words, rounder implements Python’s typical way of handling mutable and immutable objects.

rounder functions, however, make working with mutable objects simpler. For the sake of simplicity, this time we will work with simpler and smaller objects:

We did expect this, but since lists are mutable, x has been changed, too:

If you do not want this to happen, rounder can help you. You can use argument use_copy, whose default is False; that’s why on default, rounder changes the original object. This is the default behavior of Python: If you want to create a copy of an object, you have to do it manually.

Note: It’s a good place to mention shallow and deep copies. You can read about this topic in the official documentation of the standard-library copy module or here. If you need or want to deepen or refresh your knowledge about it, you can make a break and do it now.

This is how use_copy changes the behavior of rounder functions:

We can use the other rounder functions with use_copy, too:

As for rounding to significant digits, remember that this operation affects also integers:

Custom rounding

The rounder package offers also a general function, rounder.map_object(), which enables you to apply any callable that takes a number and returns a number (so, Callable[numbers.Number, numbers.Number]). You can use it to apply custom rounding.

For example, imagine you want to apply the following rounding of a floating-point number, say x:

  • if x is an integer, return it untouched
  • round to integers when abs(x) > 100
  • round to 1 decimal digit when 100 > abs(x) > 10
  • round to 2 decimal digits when 10 > abs(x) > 1
  • otherwise, round to 3 decimal digits

Such a rounding method can be used, for instance, when you round an object with numbers with different units or at different scales. The following function does this:

The function’s name, round_float, means that it will round only floats. It could be named round_only_floats, but this name would not fully represent what it does; this is because it does not round non-numbers. So, I named it round_float, to differentiate it from round.

Let’s see this in action:

Now, let’s use this function to the obj object we used above:

As you see, the function’s signature is similar to that of the map() function, the first argument being a callable to be used for an object provided as the second argument. This can help you remember how to use rounder.map_object().

Actually, you can use this function to apply any callable Callable[numbers.Number, numbers.Number] to all numbers in the object, even those having nothing to do with rounding. Consider this example:

Types

It’s worth adding that rounder‘s functions work with many various types, such as all the built-in container types (like dict, list, tuple and set), but also generators, map and filter objects, queues, named tuples, array.array, and more. You will find their list in the package’s repository. Note that when you feed in a generator type into a rounder function, you will receive a generator, too.

Exception handling

Typical rounding functions throw TypeError when you use them for non-numbers:

The rounder.signif() function behaves in a similar way:

It is not so with the rounder functions whose names end with _object:

These functions behave like this because when the object is a container of objects, the function is run recursively on them, rounding (or not, if it’s not a number) every lowest-level element of the object.

Implementation

Advanced developers may appreciate the way that rounder was developed. The idea came from Ruud van der Ham, the package’s co-author. The essence of the package is implemented using a closure, an elegant approach — much more elegant than the original class-based approach, used in the first version of the package (not released).

You can learn more about the package’s implementation from the package repository, and in particular, from its main module, rounder.

The rounder package makes rounding easy. With it, you can use the most common rounding methods:

  • rounding a number to a particular number of decimal digits (round_object());
  • rounding a number down (floor_object()) or up (ceil_object()) to the nearest integer;
  • rounding a number to a particular number of significant digits (signif() and signif_object());

but also a custom approach to rounding, thanks to

  • rounding a number using a custom function (map_object()).

But this is not what makes rounder so useful. It is so useful because you can use therounder functions whose names end with _object for any complex and/or nested object type, and the function will round all the numbers inside the object.

The package also makes it simple to work with mutable objects. Normally, when you want to create a copy of a mutable object, you need to call copy.deepcopy(), which creates a deep copy of the object. With rounder, you do not have to do it; enough to use use_copy=True argument, and the function will do it for you.

In short, whenever you need to round numbers in more complex objects than a number (so, an object following the numbers.Number abstract base class), you can find rounder particularly helpful. Its functions will also work with regular numbers. With the appearance of the rounder package, the complexity of an object in which we want to round all numbers ceased being a problem.

FOLLOW US ON GOOGLE NEWS

Read original article here

Denial of responsibility! Techno Blender is an automatic aggregator of the all world’s media. In each content, the hyperlink to the primary source is specified. All trademarks belong to their rightful owners, all materials to their authors. If you are the owner of the content and do not want us to publish your materials, please contact us by email – [email protected]. The content will be deleted within 24 hours.

Leave a comment