rspec/rules/S1244/python/rule.adoc

73 lines
3.5 KiB
Plaintext
Raw Normal View History

This rule raises an issue when direct and indirect equality/inequality checks are made on floats.
== Why is this an issue?
Floating point math is imprecise because of the challenges of storing such values in a binary representation.
In base 10, the fraction `1/3` is represented as `0.333...` which, for a given number of significant digit, will never exactly be `1/3`. The same problem happens when trying to represent `1/10` in base 2, with leads to the infinitely repeating fraction `0.0001100110011...`. This makes floating point representations inherently imprecise.
Even worse, floating point math is not associative; push a ``++float++`` through a series of simple mathematical operations and the answer will be different based on the order of those operation because of the rounding that takes place at each step.
Even simple floating point assignments are not simple, as can be vizualized using the `format` function to check for significant digits:
[source,python]
----
>>> format(0.1, ".17g")
'0.10000000000000001'
----
This can also be vizualized as a fraction using the `as_integer_ratio` method:
[source,python]
----
>>> my_float = 0.1
>>> numerator, denominator = my_float.as_integer_ratio()
>>> f"{numerator} / {denominator}"
'3602879701896397 / 36028797018963968'
----
Therefore, the use of the equality (``++==++``) and inequality (``++!=++``) operators on ``++float++`` values is almost always erroneous.
== How to fix it
Whenever attempting to compare float values, it is important to consider the inherent imprecision of floating-point arithmetic.
One common solution to this problem is to use a tolerance value (also called epsilon) to define an acceptable range of difference between two floats. A tolerance value may be relative (based on the magnitude of the numbers being compared) or absolute. Note that comparing a value to 0 is a special case: as it has no magnitude, it is important to use an absolute tolerance value.
The `math.isclose` function allows to compare floats with a relative and absolute tolerance. One should however be careful when comparing values to 0, as by default, the absolute tolerance of `math.isclose` is `0.0` (this case is covered by rule S6727)
. Depending on the library you're using, equivalent functions exist, with possibly different default tolerances (e.g `numpy.isclose` or `torch.isclose` which are respectively designed to work with `numpy` arrays and `pytorch` tensors).
If precise decimal arithmetic is needed, another option is to use the `Decimal` class of the `decimal` module, which allows for exact decimal arithmetic.
=== Code examples
==== Noncompliant code example
[source,python,diff-id=1,diff-type=noncompliant]
----
def foo(a, b):
return a == b - 0.1
----
==== Compliant solution
[source,python,diff-id=1,diff-type=compliant]
----
import math
def foo(a, b):
return math.isclose(a, b - 0.1, rel_tol=1e-09, abs_tol=1e-09)
----
== Resources
=== Documentation
* Python Documentation - https://docs.python.org/3/tutorial/floatingpoint.html#floating-point-arithmetic-issues-and-limitations[Floating Point Arithmetic: Issues and Limitations]
* Python Documentation - https://docs.python.org/3/library/decimal.html#module-decimal[Decimal fixed point and floating point arithmetic]
* NumPy Documentation - https://numpy.org/doc/stable/reference/generated/numpy.isclose.html[numpy.isclose]
* PyTorch Documentation - https://pytorch.org/docs/stable/generated/torch.isclose.html[torch.isclose]
=== Related rules
* S6727 - The `abs_tol` parameter should be provided when using `math.isclose` to compare values to `0`