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()