How to shorten ‘Decimal’ typename to ‘D’ on the output

For example, if I have a lot of type Decimal elements:

from decimal import Decimal
D = Decimal

# Generating a very log list of Decimal numbers
very_long_generated_list = [D('1.0'), D('2.9'), D('3.8')]

print(very_long_generated_list)

Will produce:

[Decimal('1.0'), Decimal('2.9'), Decimal('3.8')]

Visually parsing output with all those “Decimal” strings is quite tedious, especially if there are mathematical operations, nested structures etc. involved.

Wanted outcome is visually less taxing format for the Decimal strings:

[D('1.0'), D('2.9'), D('3.8')]

There is not always a possibility to do postprocessing of output. That is the reason I’m looking for a way to adjust the output earlier.

I noticed that decimal.Decimal is an immutable type, so it may not be possible.

def changeClassTypeName(theclass, thename):
    theclass.__class__ = type(thename, (type,), {})
  
changeClassTypeName(Decimal, 'D')

will produce

TypeError: cannot set '__class__' attribute of immutable type 'decimal.Decimal'

  • This is silly. The default outputs of built-in classes are only there for convenience and debugging. If you want to present usable output, then YOU are responsible for creating that output. Don’t just print a list. Go to the trouble of writing a short function to product the output YOU need.

    – 

It’s possible, but it envolves some Python dark magic. I don’t know if there is a better way.

Since the Decimal class is a immutable type – as you discovered by yourself – we can use a metaclass to achieve this:

(If you don’t understand what a metaclass is, just copy and paste this code into your program):

class Wrapper(type):
    def __init__(cls, name, bases, dct):
        # Helper decorator to set `f` a method of `D`
        def asmethod(f):
            setattr(cls, f.__name__, f)
            return f
        
        # Function to be called whenever a method is called on `D`
        def _wrapped(method_name):
            def _callable(self, *args, **kwargs):
                # If any argument is a `D`, unwrap its `Decimal`
                args = [arg.__obj if isinstance(arg, cls) else arg for arg in args]
                kwargs = {k: arg.__obj if isinstance(arg, cls) else arg for k, arg in kwargs.items()}
    
                # Execute the real `Decimal` operation
                real_attribute = getattr(self.__obj, method_name)
                result = real_attribute(*args, **kwargs)
                
                # If the result of the operation is a `Decimal`, wrap it into a `D`
                return cls(result) if isinstance(result, cls.__wraps__) else result
            return _callable
    
        # Whenever the user creates a `D`, set also a
        # `__obj` attribute containing the wrapped `Decimal` 
        @asmethod
        def __init__(self, obj):
            self.__obj = cls.__wraps__(obj)
        
        # Whever the user accesses an attribute/method of `D`,
        # fetch its equivalent attribute/method of `__obj`
        @asmethod
        def __getattribute__(self, name):
            if name == '_Wrapper__obj':
                return object.__getattribute__(self, name)

            real_attribute = getattr(self.__obj, name)
            if callable(real_attribute):
                function = _wrapped(name)
                return lambda *args, **kwargs: function(self, *args, **kwargs)
            return real_attribute
            
        # The logic you want: whevener the user accesses `D` as a
        # `repr`/`str`, fetch the original `repr`/`str` from
        # `__obj__`, but replacing occurences of 'Decimal' with 'D'
        @asmethod
        def __repr__(self):
            return repr(self.__obj).replace(cls.__wraps__.__name__, cls.__name__)
        @asmethod
        def __str__(self):
            return str(self.__obj).replace(cls.__wraps__.__name__, cls.__name__)

        # Whenever the user accesses an dunder method of `D`,
        # fetch its equivalent dunder method of `__obj`
        # (such as `__add__` and `__div__`)
        ignore = set("__%s__" % n for n in "class mro new init setattr getattr getattribute repr str".split())
        for name in dir(cls.__wraps__):
            # Filter only dunder methods
            if not name.startswith("__"):
                continue
            if name in ignore:
                continue  
            if name in dct:
                continue

            setattr(cls, name, _wrapped(name))

Usage:

from decimal import Decimal

class D(metaclass=Wrapper):
    __wraps__ = Decimal

print([D('1.0'), D('2.9'), D('3.8')])
# Outputs [D('1.0'), D('2.9'), D('3.8')]

# No functionality is affected!
print(D('2.3').copy_sign(D('-1.5')))  # -2.3 
print(D('3.14').sqrt())  # 1.772004514666935040199112510
print([D('1.0') + D('2.9')])  # [D('3.9')]

from decimal import Decimal

class D(Decimal):
   def __init__(self, value):
      self.value = value

   def __repr__(self):
      return f"D({self.value})"

I agree with the previous poster: use this code with caution, as you can easily confuse the hell out of another developer if you pass this along.

Leave a Comment