When building numeric analyses, one source of errors is units. It is very common to merge data from multiple sources, where each source uses a different, and incompatible set of engineering units. Once an error like this is introduced, it can be difficult to find the error and correct it.
This tutorial shows how to create a class which behaves like a floating point number, but carries a descriptive string which can be used for various purposes in a program. This is a very simple approach is provided to understand how to create a class which inherits from an immutable class in python.
Use Case:
The use case of this class is simple. It is intended to allow a unit to be attached to a floating point value when the value is passed between functions. However, the units will not be preserved through math operations. There are several libraries which will do all of this.
>>x = FloatWithUnits(3.1415,'miles')
>>y = FloatWithUnits(2.7182,'km')
>>print x
... 3.1415+0.0j miles
>>print y
... 2.7182+0.0j km
>>z = x*y
>>print z
... 8.5392253
Solution and Discussion:
Any class which emulates numeric types needs to override many class methods. By inheriting from an existing type, like a float, all of these methods will be defined with default behavior. For this problem, the challenge is to attach and engineering unit, while preserving all of the methods of a floating point number. The obvious approach to this problem is to create a class which inherits from floats.
When inheriting from an immutable type, things are a little different than inheriting from other classes. The __new__ method must generally be overridden. Otherwise, the __new__ method will prevent an overridden __init__ method from executing. Also, since we’d like the class to display properly, the __str__ and __repr__ methods are overridden. Here is the new class:
class FloatWithUnits(float): def __new__(cls,value,units=None): return float.__new__(cls,value) def __init__(self,value,units=None): self.units=units def __repr__(self): return 'FloatWithUnits('+str(self.real)+'+'+str(self.imag)+'j,"'+self.units+'")' def __str__(self): return str(self.real)+'+'+str(self.imag)+'j '+self.units if __name__=='__main__': print FloatWithUnits(4.543)**2 import unittest class Test_FloatWithUnits(unittest.TestCase): def setUp(self): pass def test_DefaultInstanciation(self): self.assertTrue(FloatWithUnits(1.45)==1.45) self.assertAlmostEqual(FloatWithUnits(1.45)*2,2*1.45) self.assertTrue(FloatWithUnits(-1.45)<0) self.assertTrue(FloatWithUnits(1.45)>0) def test_Units(self): x = FloatWithUnits(2.3,'miles') #print x.units self.assertTrue(x.units=='miles') #unittest.main() suite = unittest.TestLoader().loadTestsFromTestCase(Test_FloatWithUnits) unittest.TextTestRunner(verbosity=2).run(suite)
What doesn’t work and why.
As important as it is to see a working example, seeing a nonworking example is useful. A naive approach to solving this problem is to inherit from float and simply override the __init__ method. This will fail for several reasons, but, the most perplexing error will be the related to the number of arguments during class creation. Try the following code snippet.
class FloatWithUnits(float): def __init__(self,value,units=None): super(FloatWithUnits,self).__init__() self.units=units
>>x = FloatWithUnits(2.3,'miles’)
... TypeError: float() takes at most 1 argument (2 given)
This exception occurs because the __new__ method for float only accepts one value. The inheritance of floats in this class brings in the float __new__ method, and the __init__ method never has a chance to run.
Related Libraries and Packages:
There are several libraries which add engineering units to scalar and matrix math. Here are a few of them:
- http://pypi.python.org/pypi/units/
- This library offers the ability to turn off the units math once code is working resulting in improved performance: http://russp.us/scalar-guide.htm
- http://pypi.python.org/pypi/magnitude/0.9.1
- This library works with both scalar and matrix math in scipy/numpy: http://pypi.python.org/pypi/piquant/0.1
References:
- http://www.python.org/download/releases/2.2/descrintro/
- http://code.activestate.com/recipes/303059-overriding-__new__-for-attribute-initialization/
- http://docs.python.org/release/2.5.2/ref/numeric-types.html
All software and example codes are subject to the MIT License
Copyright (c) 2010, Ed Tate, Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
No comments:
Post a Comment