Python Forum
Adding Decimals to classes with OOP + rounding to significant digits (ATM demo)
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Adding Decimals to classes with OOP + rounding to significant digits (ATM demo)
#1
I’m learning OOP by trying to build a rudimentary banking ATM machine. It started out an an exercise for Fred Baptiste’s Udemy online courseware but I am now extending and building on top of the feature set just for fun.

Now I am endeavoring to add these features:
  • Using Decimals instead of Floats
  • “Bankers Rounding” ("half-way" amounts should be rounded up) to 2 significant digits for all transactions
This is what I am trying to accomplish today. This might sound weird but based on my testing, I’m not sure if I have succeeded. That’s why I need your help, Pythonistas!

The tutorial I am working with is titled “Python Decimal”. The tutorial demonstrates how to use decimals and rounding. Here is some sample coding running in my trusty Python REPL:

$ bpython
bpython version 0.22.1 on top of Python 3.10.4 /usr/bin/python
>>> import decimal
>>> from decimal import Decimal
>>> ctx = decimal.getcontext()
>>> ctx.rounding = decimal.ROUND_HALF_UP
>>> x = Decimal('2.3456')
>>> x
Decimal('2.3456')
>>> round(x,2)
Decimal('2.35')
>>>
So that works. Here is my script (reduced test case):

import decimal
from decimal import Decimal
from random import randint
 
class Account:
 
   def __init__(self, first_name, last_name,starting_balance=0.00):
       self.first_name = first_name
       self.last_name = last_name
       self.balance = round(Decimal(starting_balance),2)
       self.transaction_id = randint(101,999)
 
   def deposit(self, amount):
       self.balance += round(Decimal(amount),2)
       self.transaction_id += randint(101,999) - randint(101,999)
       return f'D-{self.transaction_id}'
  
   def withdraw(self, amount):         
       self.balance -= round(Decimal(amount),2)
       self.transaction_id += randint(101,999) - randint(101,999)
       return f'W-{self.transaction_id}'
Take note that in my script above I do not get a context or set the rounding to ROUND_HALF_UP. Here is me importing the script in my REPL and instantiating:

>>> import script
>>> BA = script.Account('Winston','Smith')
>>> BA.balance
Decimal('0.00')
>>> BA.deposit(0.505)
'D-547'
>>> BA.balance
Decimal('0.51')
>>> 
Here my script is rounding half up even without specifying .getcontext(). So my script is rounding up when I am expecting it to round down (Python’s default).

Here are my questions for all of you:

First of all, why is my script working when it shouldn’t be?

Secondly, for these two lines:

ctx = decimal.getcontext()
ctx.rounding = decimal.ROUND_HALF_UP
  • ...the ctx variable is an instantiation of the .getcontext() function or class method located somewhere inside the decimal package as described in the official docs. Where in this understanding am I correct or incorrect?
  • In the next line, ctx.rounding is declared based on the decimal package’s setting to ROUND_HALF_UP (among a few the other options as covered in the official Python docs). Is this correct?
My third (and perhaps my most important) question is: How and why does the above ctx instantiation have any bearing on the subsequent lines in the REPL?:

>>> x = Decimal('2.3456')
>>> x
Decimal('2.3456')
>>> round(x,2)
Decimal('2.35')
>>>
My final question is, where in my OOP script should I place / invoke the .getcontext() method from the decimal package? Inside the scope of the Account class or outside? Or would it be better suited beneath a dunder main declaration at the bottom of the script?

I guess overall here I am just struggling to grasp how, when, or where .getcontext() is used or if it is even necessary at all. Could you Pythonistas kindly clarify?
Reply
#2
The round context isn't used here because you've passed in a float initially. That float doesn't represent exactly what you want and is already higher than the target. If you want to be exact, you have to pass in a string and let Decimal do the conversion.

>>> Decimal(.505) #.505 isn't exact
Decimal('0.50500000000000000444089209850062616169452667236328125') #Not "5.05".  Is closer to 5.1
>>> round(Decimal(.505),2)
Decimal('0.51')

# Instead of float, pass in a string. '.505' is exact.
>>> Decimal('.505')
Decimal('0.505')
>>> round(Decimal('.505'),2) # now the context matters
Decimal('0.50')
>>> decimal.getcontext().rounding = decimal.ROUND_HALF_UP
>>> round(Decimal('.505'),2)
Decimal('0.51')
Drone4four likes this post
Reply
#3
Hi @bowlofred, Thank you for your reply. If I understand what you’ve said correctly, passing decimal point numbers into the Decimal() method as strings will produce the most exact and (in my case) desired output. The issue in my original example was that I was passing in floats.

So I returned to my Python REPL with the working demo. Here it is again:

$  bpython
bpython version 0.22.1 on top of Python 3.10.4 /usr/bin/python
>>> import decimal
>>> from decimal import Decimal
>>> ctx = decimal.getcontext()
>>> ctx.rounding = decimal.ROUND_HALF_DOWN
>>> x = Decimal('2.25')
>>> y = Decimal('3.35')
>>> print(round(x, 1))
2.2
>>> print(round(y, 1))
3.4
As you can see, numbers above are being passed into the Decimal() method as strings, the context is declared to round down, and everything works.

To experiment further, next is some more input and output which doesn’t make sense to me in light of what I have learned from bowlofred so far. As you can see below, I change the context rounding setting to round ‘up’ instead of ‘down’. I then proceed to redefine/reset the x variable back to a decimal using the proper format as a string. When I attempt to round x, the interpreter prints the output as rounding down when I am expecting it to round up:

>>> ctx.rounding = decimal.ROUND_HALF_UP
>>> x = Decimal('2.25')
>>> x
Decimal('2.25')
>>> print(round(x, 1))
2.2
Here is the kicker:

>>> ctx
Context(prec=28, rounding=ROUND_HALF_UP, Emin=-999999, Emax=999999, capitals=1, 
clamp=0, flags=[], traps=[DivisionByZero, Overflow, InvalidOperation])
The context variable ctx’s rounding parameter is showing that it should be rounding up so why is the output still 2.2 when it is supposed to be 2.3? I've ensured this time that the numbers being passed in to be converted as a Decimal are strings. Could someone clarify why rounding numbers are behaving this way?

I had a few other questions in my OP which remain unanswered. The burning question I am most curious to have answered is: How and why does the above ctx instantiation of the .getcontext() method (from the decimal module) have any bearing on the subsequent lines in the REPL? When .roundingis called and specified to ROUND_HALF_DOWN (or UP or EVEN), is that like a setting that is activated at run time and impacts all round() operations thereafter?
Reply
#4
Read the documentation

https://docs.python.org/3/library/decimal.html

Output:
The context for arithmetic is an environment specifying precision, rounding rules, limits on exponents, flags indicating the results of operations, and trap enablers which determine whether signals are treated as exceptions. Rounding options include ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_UP, and ROUND_05UP.
Output:
The usual start to using decimals is importing the module, viewing the current context with getcontext() and, if necessary, setting new values for precision, rounding, or enabled traps:
Reply
#5
(Apr-29-2022, 07:22 AM)Drone4four Wrote:
$  bpython
bpython version 0.22.1 on top of Python 3.10.4 /usr/bin/python
>>> import decimal
>>> from decimal import Decimal
>>> ctx = decimal.getcontext()
>>> ctx.rounding = decimal.ROUND_HALF_DOWN
>>> x = Decimal('2.25')
>>> y = Decimal('3.35')
>>> print(round(x, 1))
2.2
>>> print(round(y, 1))
3.4

I can't reproduce your output with any version of python I have. Here's 3.10.2 on MacOS:
Python 3.10.2 (main, Feb  4 2022, 17:30:41) [Clang 13.0.0 (clang-1300.0.29.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import decimal
>>> from decimal import Decimal
>>> ctx = decimal.getcontext()
>>> ctx.rounding = decimal.ROUND_HALF_DOWN
>>> x = Decimal('2.25')
>>> y = Decimal('3.35')
>>> print(round(x, 1))
2.2
>>> print(round(y, 1))
3.3
So it looks to me like you're doing everything correctly. Question: After you update ctx, does getcontext() show the change?

>>> decimal.getcontext()
Context(prec=28, rounding=ROUND_HALF_DOWN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[InvalidOperation, DivisionByZero, Overflow])
>>> ctx.rounding = decimal.ROUND_DOWN
>>> decimal.getcontext()
Context(prec=28, rounding=ROUND_DOWN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[InvalidOperation, DivisionByZero, Overflow])
If not (and I wouldn't know why the difference exists), maybe try the context manager syntax:

>>> from decimal import localcontext
>>> with localcontext() as ctx:
...     ctx.rounding = decimal.ROUND_HALF_UP
...     print(round(x,1))
...     print(round(y,1))
...
2.3
3.4
>>> with localcontext() as ctx:
...     ctx.rounding = decimal.ROUND_HALF_EVEN
...     print(round(x,1))
...     print(round(y,1))
...
2.2
3.4
Reply
#6
Bowlofred, are you running bpython like Drone4? I trust bpython even less than Idle as an environment for testing how code behaves,
Reply
#7
Nope. It says it's mainly just a wrapper around stock python, but I have no idea if it's affecting the context here.
Reply
#8
Hi @bowlofred, Thank you for your reply. I’ve done some more experimentation in my REPL based on your feedback. I tried the context manager and it turns out to behave as you predicted. It rounds up or down. Here it is:

$ bpython
bpython version 0.22.1 on top of Python 3.10.4 /usr/bin/python
>>> import decimal
>>> from decimal import Decimal
>>> ctx = decimal.getcontext()
>>> x = Decimal('2.35')
>>> y = Decimal('3.35')
>>> with decimal.localcontext() as ctx:
...     ctx.rounding = decimal.ROUND_HALF_UP
...     print(round(x,1))
...     print(round(y,1))
...     
2.4
3.4
>>> with decimal.localcontext() as ctx:
...     ctx.rounding = decimal.ROUND_DOWN
...     print(round(x,1))
...     print(round(y,1))
...     
2.3
3.3
But when we return to calling .getcontext that we were originally working with, it shows the change but Python is still rounding UP even after I tell Python to round DOWN. See here:

$ bpython
bpython version 0.22.1 on top of Python 3.10.4 /usr/bin/python
>>> import decimal
>>> from decimal import Decimal
>>> ctx = decimal.getcontext()
>>> ctx
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1
, clamp=0, flags=[], traps=[DivisionByZero, Overflow, InvalidOperation])
>>> ctx.rounding = decimal.ROUND_DOWN
>>> ctx
Context(prec=28, rounding=ROUND_DOWN, Emin=-999999, Emax=999999, capitals=1, cla
mp=0, flags=[], traps=[DivisionByZero, Overflow, InvalidOperation])
>>> x = Decimal('2.35')
>>> y = Decimal('3.35')
>>> print(round(x,1))
2.4
>>> ctx
Context(prec=28, rounding=ROUND_DOWN, Emin=-999999, Emax=999999, capitals=1, cla
mp=0, flags=[], traps=[DivisionByZero, Overflow, InvalidOperation])
>>> print(round(y,1))
3.4
>>> y
Decimal('3.35')
>>> 
So I whipped out the official python REPL and entered the same commands:

$ python
Python 3.10.4 (main, Mar 23 2022, 23:05:40) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import decimal
>>> from decimal import Decimal
>>> ctx = decimal.getcontext()
>>> ctx
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[DivisionByZero, Overflow, InvalidOperation])
>>> ctx.rounding = decimal.ROUND_DOWN
>>> ctx
Context(prec=28, rounding=ROUND_DOWN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[DivisionByZero, Overflow, InvalidOperation])
>>> x = Decimal('2.35')
>>> y = Decimal('3.35')
>>> print(round(x,1))
2.3
>>> ctx
Context(prec=28, rounding=ROUND_DOWN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[DivisionByZero, Overflow, InvalidOperation])
>>> print(round(y,1))
3.3
>>> y
Decimal('3.35')
>>> ctx.rounding = decimal.ROUND_UP
>>> ctx
Context(prec=28, rounding=ROUND_UP, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[DivisionByZero, Overflow, InvalidOperation])
>>> print(round(x,1))
2.4
>>> print(round(y,1))
3.4
>>> 
It’s working now. This proves @deanhystad’s suspicion that the problem is with the bpython REPL I am using. There seems to be a related known outstanding issue on the bpython project's GitHub repo with the decimal module. Lesson learned: Don’t trust bpython.

@deanhystad, Thank you for sharing the two specific quotes on docs.python.org. The official Python docs are vast and verbose. Even if I spent all weekend long reading the entire doc entry on the decimal package, it still probably wouldn’t have registered in my mind. So I appreciate you highlighting the specific passage relevant to my issue.

I personally find the Python docs to be written by programmers, for programmers, meaning they aren’t very helpful to novices like me. I prefer reading guides like on Geek for Geeks, Programiz, PythonTutorial.net, YouTube, and Python forums in general because in all of these locations on the web, I can find helpful guides and experienced programers who share specific practical examples written in language that is candid, approachable, and friendly rather the robotic and rigid docs.python.org.

(Apr-29-2022, 02:40 PM)deanhystad Wrote:
Output:
The context for arithmetic is an environment specifying precision, rounding rules, limits on exponents, flags indicating the results of operations, and trap enablers which determine whether signals are treated as exceptions. Rounding options include ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_UP, and ROUND_05UP.
Output:
The usual start to using decimals is importing the module, viewing the current context with getcontext() and, if necessary, setting new values for precision, rounding, or enabled traps:

I think I am beginning to understand, although I still have a few points of clarification related to these two particular passages.

When getcontext() is called and the rounding=”ROUND_UP” argument is passed in, does this establish the behavior for all rounding operations for the entire script for as long as the script is running?

So in a sense, by analogy, does .getcontext work like an ‘environmental variable’? I have a little bit of experience with environmental variables in some Django projects I have been playing with on and off recently. With Django, the DEBUG setting applies to all web apps. In my local testing environment, DEBUG is set to True which helps the developer diagnose and identify bugs. But for production, DEBUG is set to False, preventing the exposure of sensitive information to public web site visitors.

So to compare, when a .getcontext argument is passed in such as ROUND_DOWN, does the ROUND_DOWN behavior then apply to the whole script at runtime when it is called, causing all round() methods to behave the same way?

For the purposes of my script in my original post (but also copied below), I’d like all my Decimals to ROUND_HALF_UP. So where in my script should I add it? Within the scope of the Account class? Embedded beneath the dunder __init__ method? Or outside all classes near the top when all the packages are imported and initialized?

Here is my original script:

import decimal
from decimal import Decimal
from random import randint
  
class Account:
  
   def __init__(self, first_name, last_name,starting_balance=0.00):
       self.first_name = first_name
       self.last_name = last_name
       self.balance = round(Decimal(starting_balance),2)
       self.transaction_id = randint(101,999)
  
   def deposit(self, amount):
       self.balance += round(Decimal(amount),2)
       self.transaction_id += randint(101,999) - randint(101,999)
       return f'D-{self.transaction_id}'
   
   def withdraw(self, amount):     	
       self.balance -= round(Decimal(amount),2)
       self.transaction_id += randint(101,999) - randint(101,999)
       return f'W-{self.transaction_id}'
So where should the following context declaration be placed in the above script?

ctx = decimal.getcontext()
ctx.rounding = decimal.ROUND_HALF_UP
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Python won 't do decimals SomebodySmart 5 703 Jun-08-2023, 07:14 PM
Last Post: buran
  Get numpy ceil and floor value for nearest two decimals klllmmm 4 1,259 Jun-07-2023, 07:35 AM
Last Post: paul18fr
  need help rounding joseph202020 7 1,317 Feb-21-2023, 08:13 PM
Last Post: joseph202020
  from numpy array to csv - rounding SchroedingersLion 6 2,160 Nov-14-2022, 09:09 PM
Last Post: deanhystad
  floats 2 decimals rwahdan 3 1,617 Dec-19-2021, 10:30 PM
Last Post: snippsat
  Random data generation sum to 1 by rounding juniorcoder 9 3,410 Oct-20-2021, 03:36 PM
Last Post: deanhystad
  Rounding issue kmll 1 1,410 Oct-08-2021, 10:35 AM
Last Post: Yoriz
  Converting decimals stylingpat 3 2,124 Mar-27-2021, 02:32 PM
Last Post: deanhystad
  Not rounding to desired decimal places? pprod 2 2,545 Mar-05-2021, 11:11 AM
Last Post: pprod
  Decimal Rounding error project_science 4 2,750 Jan-06-2021, 03:14 PM
Last Post: project_science

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020