Closed iamthad closed 1 year 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?
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
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
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
Mutability aside, the fact that _Quantity.__hash__
is defined suggests to me that Quantities are (or were) intended to be hashable.
It seems a bug since there's an example using hash on issue #97.
Using a Quantity anywhere a hashable type is needed, such as a dictionary key, fails.
It looks like
_Quantity.__hash__
is defined, but I am not sure how that needs to be passed down to Quantity types.