The following code generates a pint.unit.DimensionalityError
exception with the following message: pint.unit.DimensionalityError: Cannot convert from 'volt' ([length] ** 2 * [mass] / [current] / [time] ** 3) to 'ohm' ([length] ** 2 * [mass] / [current] ** 2 / [time] ** 3)
. The units of measure are incompatible and you cannot sum
volts to ohms because Pint cannot perform a conversion from volts to ohms. This way, Pint makes sure you don't make mistakes by mixing incompatible units of measure.
sum = ((10 * ur.volts) + (500 * ur.ohms))
If you want to try the same example I used to described the desired behavior for a language incorporated unit of measure, you can enter the following line and Pint will also raise an exception because it cannot convert volts to ohms. In this case, the code mixes three incompatible units: volts, ohms, and inches.
# Code equivalent to the supposed language incorporated unit of measure # sum = (10 <volts> + 500 <ohms>) <inches> sum = ((10 * ur.volts) + (500 * ur.ohms)).to(ur.inches)
Pint also allows you to work with more complex units. For example, the following code specifies a speed in miles per minute and then converts it to miles per hour:
speed1 = 2 * ur.miles / ur.minute miles_per_hour = speed1.to(ur.miles / ur.hour)
You can also perform operations in compatible complex units. For example, the following code assigns 2 miles per minute to speed1
and 100 kilometers per hour to speed2
. Then, the total_speed
variable holds a Quantity
instance with the result of adding the two speed
values.
speed1 = 2 * ur.miles / ur.minute speed2 = 100 * ur.kilometers / ur.hour total_speed = speed1 + speed2
In this case, the unit of measure associated with total_speed
is miles per minute (mile / minute
for Pint) because Pint converts the other value to the unit associated with the first variable (speed1
). You have to be careful with this precedence of the operators because if you change the last line with the following line, the unit of measure associated with total_speed
is going to be kilometers per hour (kilometer / hour
for Pint) because the first variable is speed2
:
total_speed = speed2 + speed1
Thus, if you want to be sure of the unit of measure that the resulting Quantity
object will use, it is convenient to specify it, as in the next line:
total_speed = (speed2 + speed1).to(ur.miles / ur.minute)
There are many other ways to create a Quantity
instance with a value and an associated unit of measure. The following lines are equivalent and use the Quantity
constructor instead of multiplication. The Quantity
constructor can parse both units and values.
speed1 = ur.Quantity(2, ur.miles / ur.minute) speed1 = ur.Quantity(2, 'miles / minute') speed1 = ur.Quantity('2 * miles / minute')
Each time you are defining a new value with an associated unit of measure, you are creating a new instance of Quantity
; therefore, Pint introduces a performance overhead compared with working with just numeric values. However, Pint provides the necessary features to avoid the most common mistakes related to working with values expressed in different units of measure and extremely powerful customization options. In addition, the unit conversion features allow you to simplify your code and focus on your problem domain because the most common units are already defined with good accuracy.
Units: A Different Approach
Units is another Python package that allows you to work with numerical values associated to units of measure. Unit provides unit objects that you use to create Quantity
instances that save the magnitude and the associated units for any numerical type. You can perform arithmetic operations between compatible units. When you try to perform arithmetic operations on magnitudes that have incompatible units of measure, Unit raises a units.exception.IncompatibleUnitsError
exception indicating that it cannot perform the operation when the units are different. However, Units doesn't allow you to work with prefixes, so it doesn't provide a complete set of unit conversion capabilities.
Units is easy to use and includes many common units already defined without the possibility of adding prefixes. You just need to add the following lines to import unit
and predefined
from units
and call the define_units
method that initializes the most common units included in the package. You can check the predefined units by checking the source code of predefined.py
.
from units import unit, predefined units.predefined.define_units()
Then, to assign a unit of measure to a value, use the unit
object with the desired units of measure and pass the desired value as a parameter. Units will create an instance of the Quantity
class. The num
property will hold the specified value and the unit
property will include the units of measure.
You can read the following line of code as "assign 500 ohms to r1
."
r1 = unit('Ohm')(500)
If you call print(r1)
the result will be 500.00 Ohm
. If you enter r1
in the Python Console, the result will be:
Quantity(500, NamedComposedUnit('Ohm', ComposedUnit([NamedComposedUnit('V', ComposedUnit([NamedComposedUnit('W', ComposedUnit([NamedComposedUnit('J', ComposedUnit([NamedComposedUnit('N', ComposedUnit([LeafUnit('m', True), NamedComposedUnit('kg', ComposedUnit([LeafUnit('g', True)], [], 1000), False)], [LeafUnit('s', True), LeafUnit('s', True)], 1), True), LeafUnit('m', True)], [], 1), True)], [LeafUnit('s', True)], 1), True)], [LeafUnit('A', True)], 1), True)], [LeafUnit('A', True)], 1), True)).
As you can see, the way in which Units stores the composed unit is a bit complex, but you can easily check the unit with the help of the unit
object. For example, the following expression will be True
:
r1.unit == unit('Ohm')
Because Units doesn't define a kilo-Ohm
unit, if you want to be able to convert from Ohms to kilo-ohms, you need to create the kilo-Ohm
unit as a scalar multiple of the existing Ohm
unit. The following code creates the new unit and displays the value of r1
in kilo-Ohm
:
from units import scaled_unit kiloOhm = scaled_unit('kilo-Ohm', 'Ohm', 1000) print(kiloOhm(r1))
The following line displays the value of r1
expressed in ohms, no matter the unit in which r1
has the value saved.
print(unit('Ohm')(r1))
The following code uses Units to sum the values of r1
and r2
. The r1_plus_r2
variable holds the result of the sum operation expressed in ohms
and r1_plus_r2_kiloOhm
holds the result converted to the new unit defined in the previous code, kilo-Ohm
. I don't use kilo-ohms just to follow the way Units works with the predefined units, without plurals. Units performs the necessary conversion to allow you to sum the values of r1
and r2
.
ohm = unit('Ohm') r1 = ohm(500) r2 = kiloOhm(5.2) r1_plus_r2 = r1 + r2 r1_plus_r2_kiloOhm = kiloOhm(r1_plus_r2)
The following code uses Units to declare four distance
values expressed in four different units of measure: meters, miles, centimeters and feet.
distance1 = unit('m')(2500) # meters distance2 = unit('mi')(2) # miles distance3 = unit('cm')(3000) # centimeters distance4 = unit('ft')(3500) # feet
However, if you try to sum
the four distances with the following line, Units will raise a units.exception.IncompatibleUnitsError
because the four units are different.
total_distance = distance1 + distance2 + distance3 + distance4
Conclusion
Of the packages discussed in this article, Numericalunits is very simple, but it doesn't avoid the most common problems. Pint is definitely the most complete package when you just want to focus on the problem domain and take advantage of the prefixes, plurals, alias names, and the simple customization of the units definition file. Units provides basic features that you can improve by defining your own units. If you have to write an algorithm that involves complex formulas with different units of measure in Python, you will definitely want to consider one of these packages before reinventing the wheel.
Gaston Hillar is a frequent contributor to Dr. Dobb's.