Subclassing Decimal in Python
I want to use Decimal class in my Python program for doing financial calculations. Decimals to not work with floats - they need explicit conversion to strings first. So i decided to subclass Decimal to be able to work with floats without explicit conversions.
m_Decimal.py:
# -*- coding: utf-8 -*-
import decimal
Decimal = decimal.Decimal
def floatCheck ( obj ) : # usually Decimal does not work with floats
return repr ( obj ) if isinstance ( obj, float ) else obj # this automatically converts floats to Decimal
class m_Decimal ( Decimal ) :
__integral = Decimal ( 1 )
def __new__ ( cls, value = 0 ) :
return Decimal.__new__ ( cls, floatCheck ( value ) )
def __str__ ( self ) :
return str ( self.quantize ( self.__integral ) if self == self.to_integral () else self.normalize () ) # http://docs.python.org/library/decimal.html#decimal-faq
def __mul__ ( self, other ) :
print (type(other))
Decimal.__mul__ ( self, other )
D = m_Decimal
print ( D(5000000)*D(2.2))
So now instead of writing D(5000000)*D(2.2)
i should be able to write D(5000000)*2.2
without rasing exceptions.
I have several questions:
Will my decision cause me any troubles?
Reimplementing
__mul__
doesn't work in case ofD(5000000)*D(2.2)
, because the other argument is of typeclass '__main__.m_Decimal'
, but you can see in decimal module this:
decimal.py, line 5292:
def _convert_other(other, raiseit=False):
"""Convert other to Decimal.
Verifies that it's ok to use in an implicit construction.
"""
if isinstance(other, Decimal):
return other
if isinstance(other, (int, long)):
return Decimal(other)
开发者_高级运维 if raiseit:
raise TypeError("Unable to convert %s to Decimal" % other)
return NotImplemented
The decimal module expects argument being Decimal or int. This means i should convert my m_Decimal object to string first, then to Decimal. But this is lot of waste - m_Decimal is descendant of Decimal - how can i use this to make the class faster (Decimal is already very slow).
- When cDecimal will appear, will this subclassing work?
Currently, it won't do what you want at all. You can't multiply your m_decimal by anything: it will always return None, due to a missing return statement:
def __mul__ ( self, other ) :
print (type(other))
return Decimal.__mul__ ( self, other )
Even with the return added in, you still can't do D(500000)*2.2, as the float still needs converting to a Decimal, before Decimal.mul will accept it. Also, repr is not appropriate here:
>>> repr(2.1)
'2.1000000000000001'
>>> str(2.1)
'2.1'
The way I would do it, is to make a classmethod, fromfloat
@classmethod
def fromfloat(cls, f):
return cls(str(f))
Then override the mul method to check for the type of other, and run m_Decimal.fromfloat() on it if it is a float:
class m_Decimal(Decimal):
@classmethod
def fromfloat(cls, f):
return cls(str(f))
def __mul__(self, other):
if isinstance(other, float):
other = m_Decimal.fromfloat(other)
return Decimal.__mul__(self,other)
It will then work exactly as you expect. I personally wouldn't override the new method, as it seems cleaner to me to use the fromfloat() method. But that's just my opinion.
Like Dirk said, you don't need to worry about conversion, as isinstance works with subclasses. The only problem you might have is that Decimal*m_Decimal will return a Decimal, rather than your subclass:
>>> Decimal(2) * m_Decimal(2) * 2.2
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
Decimal(2) * m_Decimal(2) * 2.2
TypeError: unsupported operand type(s) for *: 'Decimal' and 'float'
There are two ways to fix this. First is to add an explicit conversion to the m_Decimal's mul magicmethod:
def __mul__(self, other):
if isinstance(other, float):
other = m_Decimal.fromfloat(other)
return m_Decimal(Decimal.__mul__(self,other))
The other way, which I probably wouldn't recommend, is to "Monkeypatch" the decimal module:
decimal._Decimal = decimal.Decimal
decimal.Decimal = m_Decimal
In my opinion you shouldn't use floats at all. Floats are not the right tool for a financial application. Anywhere you are using floats you should be able to use str or Decimal to ensure you are not losing precision.
For example.
User input, File input - obviously use str and convert to Decimal to do any math
Database - use decimal type if it is supported, otherwise use strings and convert to Decimal in your app.
If you insist on using floats, remember that python float
is equivalent to double
on many other platforms, so if you are storing them in a database for instance, make sure the database field is of type double
Use cdecimal or decimal.py from 2.7 or 3.2. All of those have the from_float class method:
class MyDecimal(Decimal):
def convert(self, v):
if isinstance(v, float):
return Decimal.from_float(v)
else:
return Decimal(v)
def __mul__(self, other):
other = self.convert(other)
return self.multiply(other)
精彩评论