Search code examples
pythonmathprecision

Trouble comparing floats in Python


I know that this should be easier, but I've having issues comparing float values.

I have some very basic code that is doing a few things:

  1. Any weights under 5 are added to light_pkgs.
  2. Any weights over 15 are added to heavy_pkgs.
  3. Any weights over 15 are removed from the original packages list.
def process_packages(package_list):
    light_pkgs = []
    heavy_pkgs = []

    light_weight = 5.00
    heavy_weight = 15.00

    for i in package_list:
        if i < light_weight:
            light_pkgs.append(i)

        elif i > heavy_weight:
            heavy_pkgs.append(i)
            packages.remove(i)

    return light_pkgs, heavy_pkgs


packages = [2.4, 3.44, 4.55, 4.9, 5.11, 5.31, 5.61, 5.99, 6.11, 7.34, 7.9, 9, 11.3, 15.1, 15.31, 15.68, 15.9, 16.7, 19.3, 21]
print(process_packages(packages))
print(packages)

I believe that due to precision errors my code isn't identifying several of the numbers in the 15.xx range.

Also, I'm limited to using built-in Python functions.

Expected Output:

([2.4, 3.44, 4.55, 4.9], [15.1, 15.31, 15.68, 15.9, 16.7, 19.3, 21])
[2.4, 3.44, 4.55, 4.9, 5.11, 5.31, 5.61, 5.99, 6.11, 7.34, 7.9, 9, 11.3]

Actual Output:

([2.4, 3.44, 4.55, 4.9], [15.1, 15.68, 16.7, 21])
[2.4, 3.44, 4.55, 4.9, 5.11, 5.31, 5.61, 5.99, 6.11, 7.34, 7.9, 9, 11.3, 15.31, 15.9, 19.3]

As you can see several numbers above 15 are not being added to the heavy_pkgs list, and aren't being removed from the original packages list.


Solution

  • You're experiencing the problem from How to remove items from a list while iterating? with extra steps.

    packages in global scope is the same thing as package_list in the function's scope (they're both aliases to the same list). When you remove from it while iterating over it, you skip iterating over the element that immediately follows the removed element (because it moves a slot down, and the iterator advances past that slot on the next loop).

    Build a new list with the light and medium weights, and replace the contents of package_list after the loop completes, and things should work (and work more efficiently, as each call to remove is O(n), so the overall work is O(n²), while a two pass build-and-replace is O(n)).

    def process_packages(package_list):
        light_pkgs = []
        not_heavy_pkgs = []  # Temporary list to hold stuff that should remain in package_list
        heavy_pkgs = []
    
        light_weight = 5.00
        heavy_weight = 15.00
    
        for i in package_list:
            if i < light_weight:
                light_pkgs.append(i)
                not_heavy_pkgs.append(i)  # Looks like light things are supposed to stay in too
            elif i > heavy_weight:
                heavy_pkgs.append(i)
            else:
                not_heavy_pkgs.append(i)  # It's not heavy, so retain it 
    
        package_list[:] = not_heavy_pkgs  # Assigning to complete slice replaces contents of
                                       # package_list in place, affecting caller's version of list
                                       # as your original code tried to do
    
        return light_pkgs, heavy_pkgs
    

    That code does get a bit unwieldy. While it would involve a couple more passes over the input, it would be much more succinct to rely on a handful of list comprehensions here:

    def process_packages(package_list):
        heavy_weight = 15.0
        # Get heavy packages
        heavy_pkgs = [pkg for pkg in package_list if pkg > heavy_weight]
        # Remove heavy packages from source
        package_list[:] = [pkg for pkg in package_list if pkg <= heavy_weight]
    
        # One more pass to get light packages and we're done
        # This pass is somewhat cheaper, because heavy items were already removed
        return [pkg for pkg in package_list if pkg < 5.0], heavy_pkgs