1

This gonna be my first question here. I am trying to make a decimal.Decimal child class which mainly differs the parent by making autonormalization on itself and on the results of its callable arguments whose returns Decimal objects. The code below have the concept to decorate all methods of Decimal to return MyDecimal instances (whose trim zeros of the end of their strings by creation) instead of decimal.Decimals. For this, metaclass was used.

However, I feel this code a bit hacky though. Moreover, according to the speed test results, it is also damn slow: 2.5 secs for the decimal.Decimal vs. 16 secs for MyDecimal on my system.

My question is: Is there a cleaner (and faster) way of doing this?

import decimal

class AutoNormalizedDecimal(type):
    def __new__(cls, name, bases, local):
        local_items = list(local.items())
        parent_items = [i for i in bases[0].__dict__.items()
            if i[0] not in local.keys()]
        for a in local_items + parent_items:
            attr_name, attr_value = a[0], a[1]
            if callable(attr_value):
                local[attr_name] = cls.decorator(attr_value)
        return super(AutoNormalizedDecimal, cls).__new__(
            cls, name, bases, local)

    @classmethod
    def decorator(cls, func):
        def wrapper_for_new(*args, **kwargs):
            new_string = args[1].rstrip('0').rstrip('.')
            if not new_string:
                new_string = '0'
            newargs = (args[0], new_string)
            return func(*newargs, **kwargs)
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            if (isinstance(result, decimal.Decimal)
                and not isinstance(result, MyDecimal)):
                return MyDecimal(str(result))
            return result
        if func.__name__ == '__new__':
            return wrapper_for_new
        return wrapper

class MyDecimal(decimal.Decimal, metaclass=AutoNormalizedDecimal):
    def __str__(self):
        return decimal.Decimal.__str__(self).replace('.', ',') 

n = MyDecimal('-5423.5420000')

def speedtest():
    import time
    start = time.time()
    d = decimal.Decimal('6')
    for i in range(1000000):
        d += decimal.Decimal(str(i))
    print(time.time()-start)

    start = time.time()
    d = MyDecimal('6')
    for i in range(1000000):
        d += MyDecimal(str(i))
    print(time.time()-start)

Here is how this works:

>>> n
Decimal('-5423.542')
>>> type(n)
<class '__main__.MyDecimal'>
>>> str(n)
'-5423,542'
>>> x = MyDecimal('542.63') / MyDecimal('5.2331')
>>> x
Decimal('103.6918843515315969501824922')
>>> type(x)
<class '__main__.MyDecimal'>
>>> y = MyDecimal('5.5252') - MyDecimal('0.0052')
>>> y
Decimal('5.52')
>>> z = decimal.Decimal('5.5252') - decimal.Decimal('0.0052')
>>> z
Decimal('5.5200')

Thanks in advance!

PS: Credit goes to Anurag Uniyal for his code which gave me a way to start: https://stackoverflow.com/a/3468410/2334951

EDIT1: I came out to redefine as_tuple() method which I could call all the time I need the trimmed Decimal version:

class MyDecimal(decimal.Decimal):

    def as_tuple(self):
        sign, digits_, exponent = super().as_tuple()
        digits = list(digits_)
        while exponent < 0 and digits[-1] == 0:
            digits.pop()
            exponent += 1
        while len(digits) <= abs(exponent):
            digits.insert(0, 0)
        return decimal.DecimalTuple(sign, tuple(digits), exponent)

    def __str__(self):
        as_tuple = self.as_tuple()
        left = ''.join([str(d) for d in as_tuple[1][:as_tuple[2]]])
        right = ''.join([str(d) for d in as_tuple[1][as_tuple[2]:]])
        return ','.join((left, right))
Community
  • 1
  • 1
SzieberthAdam
  • 3,999
  • 2
  • 23
  • 31
  • 2
    Subclassing is the **wrong** way of doing that! Simply define a function `pretty_decimal_str(dec_number)` that returns the string in the format you want. The speed decrease is caused by the fact that you have doubled the number of method calls and the code also creates more objects. More dynamism you use the less efficient is the code(generally), thus if you want performances try to avoid hacky code. – Bakuriu Jun 19 '13 at 15:46
  • Thanks for pointing that Bakuriu, you are absolutely right in this case! However I think I really need the dynamism for my current project, and I tried to get some metaclassing experiment which I haven't had before. As a workaround I made a new class which satisfies my needs nicely. Check EDIT1... – SzieberthAdam Jun 19 '13 at 16:01
  • 1
    have you tried `Decimal.normalize()` method? – jfs Jun 19 '13 at 16:13
  • As title sais my initial concept was to make an autonormalized version so I won't need to bother calling normalize() again and again. As in EDIT1 version I also sought for flexibility but since I need to call Decimal.as_tuple() anyway when I need to maipulate decimals to make their sum equal to a base number, I could spare creating another object by calling Decimal.normalize() first. Not that I would not creating many of them in my first concept... :) – SzieberthAdam Jun 19 '13 at 16:25
  • `.normalize()` calls makes the loop in `speedtest()` only 50% slower, not 6 times slower as MyDecimal(). – jfs Jun 19 '13 at 20:13
  • I ended up using a pretty_tupled(decimalobj) function which works similar like the redefined MyDecimal.as_tuple() of EDIT1 version, and a prettystr(decimalobj) which uses .normalize() instead of anything fancy. I made this because I realized that managing MyDecimals which still returns decimal.Decimals would going to be hell, as I assumed it for the first time when I tried to metaclassing them to avoid this. However, it seems that this ended up a nice example to show: **Keep things simple, no matters what!** Thanks for your comments Sebastian and Bakuriu! – SzieberthAdam Jun 19 '13 at 20:40

0 Answers0