hgrecco / pint

Operate and manipulate physical quantities in Python
http://pint.readthedocs.org/
Other
2.4k stars 468 forks source link

Quantity is unhashable #576

Closed iamthad closed 1 year ago

iamthad commented 6 years ago

Using a Quantity anywhere a hashable type is needed, such as a dictionary key, fails.

>>> import pint
>>> units = pint.UnitRegistry()
>>> {units.Quantity('10 m'): 1}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'Quantity'

It looks like _Quantity.__hash__ is defined, but I am not sure how that needs to be passed down to Quantity types.

onealj commented 6 years ago

In the Java world, generally an object is hashable only if it is immutable. If an object is mutated after it is added to a dictionary, it may not be possible to look up that object in O(1) time. At worst, it may no longer be possible to find the object.

Is a Quantity immutable?

iamthad commented 6 years ago

It looks like Quantity is in fact mutable.

>>> q2 = units.Quantity(10, 'mm')
>>> q2
<Quantity(10, 'millimeter')>
>>> id(q2)
140419432995304
>>> q2.ito_base_units()
>>> q2
<Quantity(0.01, 'meter')>
>>> id(q2)
140419432995304
onealj commented 6 years ago

More evidence that a Quantity is mutable, due to in-place operations that change the fundamental quantity (not just the representation via a unit conversion, which could be avoided by hashing a quantity represented in canonical (base) units).

The in-place operators are: iadd_sub(quantity), ito(unit), ito_base_units(), and the dunder methods __iadd__, __isub__, __imul__, __idiv__, __ifloordiv__, __itruediv__, __ipow__

>>> pint.__version__
'0.4'
>>> ureg = pint.UnitRegistry()
>>> q = 10 * ureg.mm
>>> q
<Quantity(10, 'millimeter')>
>>> id(q)
140276778417232
>>> q += 1*ureg.mm
>>> q
<Quantity(11, 'millimeter')>
>>> id(q)
140276778417232
onealj commented 6 years ago

If you really need a hashable quantity, you could convert Quantity to an immutable object and use ImmutableQuantities in your code, or you could monkey-patch the hash implementation, so long as you don't change the quantity.

class ImmutableQuantity(pint.unit.Quantity):
    def __iadd__(self, other):
        raise NotImplementedError('In-place modification is not allowed for immutable quantities')
    # override other in-place functions

    def __hash__(self):
        # Decide whether you want 10mm and and 0.1m quantities to be considered equal
        canonical_quantity = self.to_base_units()
        m = canonical_quantity.magnitude
        u = tuple(canonical_quantity.units.items())
        return hash((m, u))

    def __eq__(self, other):
        # Decide whether you want 10mm and and 0.1m quantities to be considered equal
        # Equal objects must have the same hash
        return isinstance(other, pint.unit.Quantity) and self.to_base_units() == other.to_base_units()
    # total_ordering, or define other equality functions
iamthad commented 6 years ago

Mutability aside, the fact that _Quantity.__hash__ is defined suggests to me that Quantities are (or were) intended to be hashable.

johnnywell commented 6 years ago

It seems a bug since there's an example using hash on issue #97.