0
\$\begingroup\$

I have been writing a math library with more options than the standard builtin one, partially to make my life easier in the future, and partially just for practice. Here is what I have so far in the main file (master/xtrmth/xm.py):


import typing as tp
import decimal as dc

_meganum = tp.Union[int, float, dc.Decimal, tp.SupportsInt, tp.SupportsFloat]
_num = tp.Union[int, float, dc.Decimal]
_micronum = tp.Union[int, float]
_decnum = tp.Union[float, dc.Decimal]

def _total_round(value: _num, precision: int = 10, decimal: bool = False) -> _num:
    """Rounds 'value' to the nearest 'precision' digits."""
    if isinstance(value, int):
        return value
    elif precision < 0:
        raise ValueError('Cannot cast a negative integer onto \'xm._total_round(precision)\'')
    elif decimal is True:
        if isinstance(value, dc.Decimal):
            return round(value, precision)
        elif not isinstance(value, dc.Decimal) and decimal is True:
            raise TypeError('Cannot cannot cast \'float\' onto \'xm._total_round(value)\' \
with opperand \'decimal\' as \'True\'.')
    elif decimal is False:
        if isinstance(value, float):
            return round(value, precision)
        elif not isinstance(value, float):
            raise TypeError('Cannot cast \'decimal\' onto \'xm._total_round(value)\' \
with opperand \'decimal\' as \'False\'.')
    
def summation(count: int, bottom_var: str, expression: str, precision: int = 10, \
decimal: bool = False) -> _num:
    '''Summation function. Example: 'summation(4, 'z=1', 'z+1')' would return 14.'''
    
    if precision < 0:
        raise ValueError('Cannot cast a negative integer onto \'xm.summation(precision)\'')
    var, value = bottom_var.split('=')
    var = var.strip()
    
    if decimal is True:
        value = dc.Decimal(eval(value))
    else:
        value = int(eval(value))

    res = 0
    for i in range(value, count+1):
        res += eval(expression.replace(var, str(i)))

    if decimal is True:
        return _total_round(value=res, precision=precision, decimal=True)
    return _total_round(res, precision=precision, decimal=False)

def sq(value: _num, precision: int = 10, decimal: bool = False, _print_unround: bool = False) -> _micronum:
    '''Returns 'value' raised to the 2nd power, with 'precision' decimal points.'''
    if isinstance(value, float) and decimal is True:
        raise TypeError('Cannot cannot cast \'float\' onto \'xm.cb\' \
with opperand \'decimal\' as \'True\'.')
    elif isinstance(value, dc.Decimal) and decimal is False:
        raise TypeError('Cannot cannot cast \'decimal\' onto \'xm.cb\' \
with opperand \'decimal\' as \'False\'.')
    elif isinstance(value, dc.Decimal) and decimal is True:
        if _print_unround is True:
            print(value*value)
        return _total_round(value*value, precision, decimal=True)
    if _print_unround is True:
        print(value*value)
    return _total_round(value*value, precision, decimal=False)

def sqrt(value: _meganum, precision: int = 10, decimal: bool = False, _print_unround: bool = False) -> _num:
    if decimal is True:
        x = dc.Decimal(value)
        y = dc.Decimal(1)
        e = dc.Decimal(0.000000000000000000000000000000000000000000000000000000000000000001)
    else:
        x = value
        y = 1
        e = 0.0000000000000000000000001
    
    while x - y > e:
        x = (x + y)/2
        y = value / x

    if _print_unround is True:
        print(x)

    return(_total_round(x, precision, decimal=decimal))

def cb(value: _meganum, precision: int = 10, decimal: bool = False, _print_unround: bool = False) -> _num:
    '''Returns 'value' raised to the 2nd power, with '''
    if isinstance(value, float) and decimal is True:
        raise TypeError('Cannot cannot cast \'float\' onto \'xm.cb\' \
with opperand \'decimal\' as \'True\'.')
    elif isinstance(value, dc.Decimal) and decimal is False:
        raise TypeError('Cannot cannot cast \'decimal\' onto \'xm.cb\' \
with opperand \'decimal\' as \'False\'.')
    elif isinstance(value, dc.Decimal) and decimal is True:
        if _print_unround is True:
            print(value*value*value)
        return _total_round(value*value*value, precision, decimal=True)
    if _print_unround is True:
        print(value*value*value)
    return _total_round(value*value*value, precision, decimal=False)

def cbrt(value, _print_unround: bool = False) -> _num:
    x = value**(1/3)

    if _print_unround is True:
        print(x)

    if type(x) is float:
        if round(x, 10) == int(round(x, 10)): return int(round(x, 10))
        return round(x, 10)
    return x

def xpn(base: _meganum, exponent: _meganum, decimal: bool = False, precision: int = 10, _print_debug: bool = False) \
-> _num:
    '''Raises 'base' to the power of 'exponent'.'''
    
    if not isinstance(base, dc.Decimal) and decimal is True:
        raise TypeError(f'Cannot cast \'{type(base).__name__()}\' onto \'xm.xpn(base)\' with opperand \'decimal\' as \'True\'')
    elif isinstance(base, dc.Decimal) and decimal is False:
        raise TypeError('Cannot cast \'decimal.Decimal\' onto \'xm.xpn(base)\' with opperand \'decimal\' as \'False\'')
        
    out = 1
    
    if isinstance(exponent, int):
        if _print_debug is True:
            print('exponent is int')
        for i in range(exponent):
            if _print_debug is True:
                print(out)
            out *= base
        return _total_round(out, precision=precision, decimal=decimal)
    else:
        # will update with my own algorithim in a later update
        return _total_round(base**exponent, precision=precision, decimal=decimal)

def rt(base: _meganum, root: _meganum, precision: int = 10, decimal: bool = False, _print_debug: bool = False) -> _num:
    '''Takes the 'root' root of 'base' '''

    if isinstance(base, dc.Decimal) and decimal is False:
        raise TypeError('Cannot cast \'decimal.Decimal\' onto \'xm.rd(base)\' with opperand \'decimal\' as \'False\'')
    elif not isinstance(base, dc.Decimal) and decimal is True:
        raise TypeError(f'Cannot cast \'{type(base).__name__}\' onto \'xm.rd(base)\' with opperand \'decimal\' as \'False\'')
    elif isinstance(root, dc.Decimal) and decimal is False:
        raise TypeError('Cannot cast \'decimal.Decimal\' onto \'xm.rd(root)\' with opperand \'decimal\' as \'False\'')
    elif not isinstance(root, dc.Decimal) and decimal is True:
        raise TypeError(f'Cannot cast \'{type(root).__name__}\' onto \'xm.rd(root)\' with opperand \'decimal\' as \'True\'')
    
    if decimal is True:
        return xpn(base = base, exponent = (dc.Decimal(1) / root), decimal = True, precision = precision, _print_debug = _print_debug)
    return xpn(base = base, exponent = (1 / root), decimal = False, precision = precision, _print_debug = _print_debug)

Is there anything I should change?

Github page: Github
PyPI page: PyPI

\$\endgroup\$
4
  • \$\begingroup\$ cb(…) -> _num: '''Returns 'value' raised to the 2nd power, with ''' Again? \$\endgroup\$
    – greybeard
    Jan 25, 2021 at 10:42
  • \$\begingroup\$ @greybeard That was a mistype in the docstring. \$\endgroup\$ Jan 25, 2021 at 13:43
  • \$\begingroup\$ (I'd call it a C&PE: Copy&Paste Error. In the "original code", probably, rather than in transferring to SE. Keep in mind a) posting on SE puts contents under a Creative Commons licence b) docstrings are available during execution via introspection.) \$\endgroup\$
    – greybeard
    Jan 25, 2021 at 13:57
  • \$\begingroup\$ @greybeard No, I simply made a mistype. It's not that rare. \$\endgroup\$ Jan 25, 2021 at 14:28

2 Answers 2

3
\$\begingroup\$

General

import typing as tp => from typing import Union reduces acronyms reader needs to remmber

Backslashes are scary. Adding parentheses or in this case trusting the existing ones will be easier to read.

def summation(count: int, bottom_var: str, expression: str, precision: int = 10, \
decimal: bool = False) -> _num:

Again, backslashes are scary.

raise ValueError('Cannot cast a negative integer onto \'xm.summation(precision)\'')
# could be
raise ValueError("Cannot cast a negative integer onto 'xm.summation(precision)'")

There are a lot of underscores here. Exposing the important functions in an separate file would reduce the need to hide everything.

Functions

Each of your functions are written in a pretty similar style, so I'll just look at one of them.

def rt(base: _meganum, root: _meganum, precision: int = 10, decimal: bool = False, _print_debug: bool = False) -> _num:
    '''Takes the 'root' root of 'base' '''

    if isinstance(base, dc.Decimal) and decimal is False:
        raise TypeError('Cannot cast \'decimal.Decimal\' onto \'xm.rd(base)\' with opperand \'decimal\' as \'False\'')
    elif not isinstance(base, dc.Decimal) and decimal is True:
        raise TypeError(f'Cannot cast \'{type(base).__name__}\' onto \'xm.rd(base)\' with opperand \'decimal\' as \'False\'')
    elif isinstance(root, dc.Decimal) and decimal is False:
        raise TypeError('Cannot cast \'decimal.Decimal\' onto \'xm.rd(root)\' with opperand \'decimal\' as \'False\'')
    elif not isinstance(root, dc.Decimal) and decimal is True:
        raise TypeError(f'Cannot cast \'{type(root).__name__}\' onto \'xm.rd(root)\' with opperand \'decimal\' as \'True\'')
    
    if decimal is True:
        return xpn(base = base, exponent = (dc.Decimal(1) / root), decimal = True, precision = precision, _print_debug = _print_debug)
    return xpn(base = base, exponent = (1 / root), decimal = False, precision = precision, _print_debug = _print_debug)
  • You can afford more than 2 characters for a function name.
  • The Decimal type as an edge case clutters up both your usage and your library code.
  • The conditional statements can be removed by taking advantage of features these types already have
def nth_root(base: _meganum, n: _meganum, precision: int = 10) -> _num:
    '''Takes the nth root of 'base' '''
    # works for Decimals, ints, floats, whatever
    exponent = n ** -1
    return xpn(base=base, exponent=exponent, precision=precision)
\$\endgroup\$
1
\$\begingroup\$

What's going on here?

elif decimal is True:
    ⋮
elif decimal is False:

Firstly, especially since we annotated that decimal is boolean, we should just be testing its truthiness; secondly, at the elif we already know that decimal isn't true. So, simply write

elif decimal:
    ⋮
else:

Applying this simple change throughout already cuts a lot of unnecessary verbiage and improves the readability.

The immediately preceding code shows we shouldn't be in elif anyway

if isinstance(value, int):
    return …
elif precision < 0:
    raise …
elif decimal is True:

Since return and raise both exit the flow of control, we can reduce cognitive load thus:

if isinstance(value, int):
    return …
if precision < 0:
    raise …
if decimal:

Looking inside the if decimal case, we see:

  elif decimal is True:
        if isinstance(value, dc.Decimal):
            return …
        elif not isinstance(value, dc.Decimal) and decimal is True:
            raise …

In last condition, we know that decimal is true and that value is not a dc.Decimal, so that one just becomes a plain else - and not needed because the previous clause returned:

  elif decimal:
        if not isinstance(value, dc.Decimal):
            raise …
        return …

Just simplifying the control flow in that one function makes it a lot simpler:

def _total_round(value: _num, precision: int = 10, decimal: bool = False) -> _num:
    """Rounds 'value' to the nearest 'precision' digits."""
    if isinstance(value, int):
        return value
    if precision < 0:
        raise ValueError("Cannot cast a negative integer onto 'xm._total_round(precision)'")
    if decimal and not isinstance(value, dc.Decimal):
        raise TypeError("Cannot cannot cast 'float' onto 'xm._total_round(value)' \
with operand 'decimal' as 'True'.")
    if not decimal and not isinstance(value, float):
        raise TypeError("Cannot cast 'decimal' onto 'xm._total_round(value)' \
with operand 'decimal' as 'False'.")
    return round(value, precision)

Looking again from further away, I don't see any unit-tests at all. What happened to them?

\$\endgroup\$
4
  • \$\begingroup\$ To most of this: understood, I will change (note though that _total_round() has already been greatly simplified since I made this post). To the unit tests: I usually just go into the REPL, import the file, and try out the functions. \$\endgroup\$ Mar 15, 2021 at 16:18
  • \$\begingroup\$ A good regression suite is well worth cultivating, and in Python it's easy to include it under if __name__ == '__main__'. Definitely consider writing some tests - and for your next project, they are even more valuable if you use them to write the code! \$\endgroup\$ Mar 15, 2021 at 16:50
  • \$\begingroup\$ I just left the ugly wrapped strings as is, because I was focusing on the control flow. The other answer deals with that more effectively (Python isn't my main language, so I didn't want to over-step my competence!) \$\endgroup\$ Mar 22, 2021 at 10:11
  • \$\begingroup\$ @TobySpeight Sorry didn't get pung. Oh, I thought you were the one to change the strings... I see now I was wrong. Sorry. \$\endgroup\$
    – Peilonrayz
    Mar 31, 2021 at 21:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.