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
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
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
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
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 = 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
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
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.
The following code uses Units to sum the values of
r1_plus_r2 variable holds the result of the sum operation expressed in
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
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
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.