Welcome, guest | Sign In | My Account | Store | Cart

Programs that deal with measured quantities usually make an implicit assumption of the measurement unit, a practice which is error prone, inflexible and cumbersome. This metaclass solution takes the burden of dealing with measurement units from the user, by associating each quantity to a unit of some measure. Operations on instances of these measures are unit-safe; moreover, unit conversions take place implicitly.

Python, 221 lines
 ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221``` ```def demo(): # The definition of measure classes is straightforward, for example: class Length(object): __metaclass__ = MeasureType DEFAULT_UNIT = "m" _TO_DEFAULT = { "mm":0.001, "cm":0.01, "km":1000, "in":0.0254, "ft":0.3048, "yd":0.9144, "mi":1609.34 } l1 = Length(12.3,'cm') l2 = Length(50,'mm') Length.setDefaultUnit("in") print l1+l2 # "6.811024 in" print 3*l2.value("m") # "0.15" print l1>l2 # "True" # Measures with arbitrary unit conversion functions, like temperature, # are also supported: class Temperature(object): __metaclass__ = MeasureType DEFAULT_UNIT = "Celsius" _TO_DEFAULT = { "Fahrenheit" : lambda tf: 5.0/9.0 * (tf-32) } _FROM_DEFAULT = { "Fahrenheit" : lambda tc: 9.0/5.0 * tc + 32 } #todo: # - support for unit prefixes (e.g. "c" for 10^-2, "k" for 10^3", etc.); # thus "cm", "km", etc. won't have to be specified for Length # - unit name aliases (e.g. "kilometers","km") # - compound measures: # a. Compound measure definition (e.g. Velocity) # b. Modify *,/ to be allowed between any Measure instances (e.g. Length/Duration) # c. Combine a and b (?) # - thorough unit testing __author__ = "George Sakkis" class MeasureType(type): '''Metaclass for supporting unit-safe measure objects. Classes generated with this as metaclass (e.g. Length, Area, Temperature, etc.) must define the following class-scope variables: 1. DEFAULT_UNIT (a string), and 2a. either _TO_DEFAULT, a dictionary that maps each other unit to - either a constant number, which is interpreted as the ratio of the unit to the DEFAULT_UNIT - or a function, for more complicate conversions from the unit to DEFAULT_UNIT (e.g. from Celsius to Fahrenheit). 2b. or _FROM_DEFAULT. Inverse of _TO_DEFAULT, maps each unit (other than the DEFAULT_UNIT) to the function that returns the measure in this unit, given the DEFAULT_UNIT (e.g. from Fahrenheit to Celsius). If there is at least one function present, both _TO_DEFAULT and _FROM_DEFAULT have to be given (so that both the function and its inverse are defined).''' def __new__(cls,classname,bases,classdict): defUnit = classdict["DEFAULT_UNIT"] toDefault = classdict.get("_TO_DEFAULT") or {} fromDefault = classdict.get("_FROM_DEFAULT") or {} units = uniq([defUnit] + toDefault.keys() + fromDefault.keys()) restUnits = units[1:] # form convertion matrix conversion_matrix = {} # 1. non-default unit -> default unit for unit in restUnits: try: conversion_matrix[(unit,defUnit)] = toDefault[unit] except KeyError: try: conversion_matrix[(unit,defUnit)] = 1.0 / fromDefault[unit] except (KeyError,TypeError): raise LookupError, "Cannot convert %s to %s" % (unit,defUnit) # 2. default unit -> non-default unit for unit in restUnits: # check if toDefault[unit] is number try: conversion_matrix[(defUnit,unit)] = fromDefault[unit] except KeyError: try: conversion_matrix[(defUnit,unit)] = 1.0 / toDefault[unit] except (KeyError,TypeError): raise LookupError, "Cannot convert %s to %s" % (defUnit,unit) # 3. non-default unit -> non-default unit for fromUnit in restUnits: for toUnit in restUnits: if fromUnit != toUnit: try: conversion_matrix[(fromUnit,toUnit)] = conversion_matrix[(fromUnit,defUnit)] \ * conversion_matrix[(defUnit,toUnit)] except TypeError: conversion_matrix[(fromUnit,toUnit)] = \ lambda value: conversion_matrix[(defUnit,toUnit)]( conversion_matrix[(fromUnit,defUnit)](value)) ## print conversion_matrix # 4. convert all constants c to functions lambda x: c*x for (k,v) in conversion_matrix.iteritems(): try: conversion_matrix[k] = lambda x,i=float(v): x*i except (TypeError,ValueError): pass # update classdict assert "_CONVERT_UNITS" not in classdict classdict["_CONVERT_UNITS"] = conversion_matrix classdict["_UNITS"] = units if "_TO_DEFAULT" in classdict: del classdict["_TO_DEFAULT"] if "_FROM_DEFAULT" in classdict: del classdict["_FROM_DEFAULT"] # set Measure to be base a class of cls bases = tuple(uniq((MeasureType.Measure,) + bases)) return type.__new__(cls,classname,bases,classdict) class Measure(object): '''The superclass of every Measure class. Concrete measure classes don't have to specify explicitly this class as their superclass; it is added implicitly by the MeasureType metaclass.''' __slots__ = "_value", "_unit" def setDefaultUnit(cls,unit): if unit != cls.DEFAULT_UNIT: cls.__verifyUnit(unit) cls.DEFAULT_UNIT = unit setDefaultUnit = classmethod(setDefaultUnit) def value(self, unit=None): if unit == None: unit = self.__class__.DEFAULT_UNIT self.__verifyUnit(unit) return self.__convert(self._value, self._unit, unit) def __init__(self, value, unit=None): if unit == None: unit = self.__class__.DEFAULT_UNIT self.__verifyUnit(unit) self._unit = self.__class__.DEFAULT_UNIT self._value = self.__convert(value, unit, self._unit) def __str__(self,unit=None): if unit == None: unit = self.__class__.DEFAULT_UNIT return "%f %s" % (self.value(unit), unit) def __abs__(self): return self.__class__(abs(self._value), self._unit) def __neg__(self): return self.__class__(-self._value, self._unit) def __cmp__(self,other): try: return cmp(self._value, self.__coerce(other)) except ValueError: return cmp(id(self),id(other)) def __add__(self,other): try: return self.__class__(self._value + self.__coerce(other), self._unit) except ValueError: raise ValueError, "can only add '%s' (not '%s') to '%s'" \ % (self.__class__.__name__, other.__class__.__name__, self.__class__.__name__) def __sub__(self,other): try: return self.__class__(self._value - self.__coerce(other), self._unit) except ValueError: raise ValueError, "can only subtract '%s' (not '%s') from '%s'" \ % (self.__class__.__name__, other.__class__.__name__, self.__class__.__name__) def __iadd__(self,other): try: self._value += self.__coerce(other) except ValueError: raise ValueError, "can only increment '%s' (not '%s') by '%s'" \ % (self.__class__.__name__, other.__class__.__name__, self.__class__.__name__) def __isub__(self,other): try: self._value -= self.__coerce(other) except ValueError: raise ValueError, "can only decrement '%s' (not '%s') by '%s'" \ % (self.__class__.__name__, other.__class__.__name__, self.__class__.__name__) def __mul__(self,other): try: return self.__class__(self._value * float(other), self._unit) except TypeError: raise ValueError, "can only multiply '%s' (not '%s') by a number" \ % (self.__class__.__name__, other.__class__.__name__) __rmul__ = __mul__ def __div__(self,other): try: return self.__class__(self._value / float(other), self._unit) except TypeError: raise ValueError, "can only divide '%s' (not '%s') by a number" \ % (self.__class__.__name__, other.__class__.__name__) def __imul__(self,other): try: self._value *= float(other) except TypeError: raise ValueError, "can only multiply '%s' (not '%s') by a number" \ % (self.__class__.__name__, other.__class__.__name__) def __idiv__(self,other): try: self._value /= float(other) except TypeError: raise ValueError, "can only divide '%s' (not '%s') by a number" \ % (self.__class__.__name__, other.__class__.__name__) def __pow__(self,other): try: return self.__class__(self._value ** float(other), self._unit) except TypeError: raise ValueError, "can only raise '%s' to a number (not '%s')" \ % (self.__class__.__name__, other.__class__.__name__) def __ipow__(self,other): try: self._value **= float(other) except TypeError: raise ValueError, "can only raise '%s' to a number (not '%s')" \ % (self.__class__.__name__, other.__class__.__name__) ################################## 'private' Methods ################################## def __convert(cls, value, fromUnit, toUnit): if fromUnit == toUnit: return value else: return cls._CONVERT_UNITS[(fromUnit,toUnit)](value) __convert = classmethod(__convert) def __verifyUnit(cls,unit): if unit not in cls._UNITS: raise ValueError, "'%s' is not a recognized %s unit (pick one from %s)" % \ (unit, cls.__name__, ', '.join(["'%s'" % u for u in cls._UNITS])) __verifyUnit = classmethod(__verifyUnit) def __coerce(self,other): if type(self) != type(other) or not isinstance(self,MeasureType.Measure): raise ValueError return self.__convert(other._value,other._unit,self._unit) def uniq(sequence): '''Removes the duplicates from the given sequence, preserving the order of the remaining elements.''' dict = {} try: return [dict.setdefault(i,i) for i in sequence if i not in dict] except TypeError: # unhashable item(s) return [dict.setdefault(repr(i),i) for i in sequence if repr(i) not in dict] if __name__ == '__main__': demo() ```

The usual way of dealing with measures of various kinds (e.g. length, angle, duration,etc.) is by implying a fixed unit (e.g. meters,rad,seconds) and using simple float numbers for the quantities. That's not a problem as long as all parts of the program use the same unit (though shifting to another unit may require changes in several parts). However, it is much more error-prone if more than one units of the same measure are involved, and different modules interpret such numbers in different ways. Also, applying the necessary unit conversions explicitly results in less clear code.

This metaclass solution takes the burden of dealing with units from the user, by associating each quantity to a unit of some measure. Operations on instances of these measures are unit-safe; moreover, unit conversions take place implicitly.

The release is in alpha state; the documentation is scarce and there may be some bugs around. Also, there are several possible useful extensions to be done (see the comments for a tentative "todo" list). Tim Hoffman 19 years, 7 months ago

Have a look at Unum heaps of work already done. Hi

Its good to see these types of advanced recipes, but if you are interested in this type of thing it would probably pay to have a look at the Unum module.

http://home.tiscali.be/be052320/Unum.html

Tim George Sakkis (author) 19 years, 7 months ago

As you can guess, I didn't know of it; it looks neat indeed. Thanks, I'll check it out.

George henry crun 19 years, 7 months ago

There was a very good HP/Agilent paper on the topic of units and unit maths.

This is very useful for interactive use. For programs you would often want to use it like an assert ie waste time doing unit computations during debug or first run. David Eyk 16 years ago

Keeping units with values. Thanks for the great recipe. I've modified value() to return a 2-tuple of (value, units), which is easy enough to plug back in to another Measure. This way, you're not discarding information.

My physics teacher always got mad at us when we dropped the units from a number. "5 what?" he'd ask. "5 candy bars? 5 elephants? 5 meters/second?" Created by George Sakkis on Mon, 23 Feb 2004 (PSF)

### Required Modules

• (none specified)