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.
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).
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
As you can guess, I didn't know of it; it looks neat indeed. Thanks, I'll check it out.
George
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.
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?"