patternpythonMinor
Multiplication or addition of decimals or integers for prehighschool; non deterministic testing
Viewed 0 times
decimalsprehighschoolmultiplicationnonadditiontestingfordeterministicintegers
Problem
I'm creating a simple android app for pre-highschool students which teaches the very basics of addition or multiplication of integers or decimals.
The part of the program in this question is aimed at allowing them to practice without the need of a teacher giving them exercises and checking their results.
Note: This is the first time I m using unit tests so I'm not experienced at unit-testing at all.
The following are of great interest to me:
feels more of a fixation on following blindly a "rule" without the
actual need for it (but of course I might be wrong).
Of course any other comments unrelated to the above 2 points are also very welcome.
Directory tree
Code
Main module:
```
"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)
Some examples of "questions" and their "answers":
"+
The part of the program in this question is aimed at allowing them to practice without the need of a teacher giving them exercises and checking their results.
Note: This is the first time I m using unit tests so I'm not experienced at unit-testing at all.
The following are of great interest to me:
- Unit tests:
- What should I improve? I don't aim for 100% code coverage since that
feels more of a fixation on following blindly a "rule" without the
actual need for it (but of course I might be wrong).
- Since my methods contain
random.random()numbers I was thinking of creating tests that test a method millions or billions of times (e.g.assertIsInstance(_int_term(), int)inside afor _ in range(10**8)loop). This would very strongly indicate (but not with absolute certainty) that the methods work correctly. Should I create such tests?
- Methods with optional parameters, e.g.
Terms._int_term(max_val): The parameters are made optional only because I wanted to be able to test the method easily (that is, call it without the need to instantiateTerms()class. If I were not using unit tests, I would simply make it a non static method, and would remove the parameter completely. Is this a bad practice?
- Docstrings: Are more docstrings needed? Or do good method names suffice? The reason I didn't create dosctings for everything is that they often go stale when I change code without also updating all the related docstrings.
Of course any other comments unrelated to the above 2 points are also very welcome.
Directory tree
- project_dir
- main.py
- tests
- test_main
- test_questionandanswer.py
- test_terms.py
Code
Main module:
```
"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)
Some examples of "questions" and their "answers":
"+
Solution
Style
Python has a style guide called PEP 8 which is definitly worth reading and and worth following if you do not have good reasons not to. In you case, your usage of empty lines for instance is not compliant to PEP8. You'll find tools online to check your code compliancy to PEP8 in a automated way if you want to.
Code organisation
You've written your code in classes which is sometimes a good thing. Sometimes, you don't need a class.
In any case, most of the things in the
Also, you have variables that are not used at all and you should get rid of them.
Once everything than can be moved out of the class (and renamed) is moved out, you get:
```
"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)
Some examples of "questions" and their "answers":
"+2-4" , "-2"
"(-2)(+7)", "-14"
"-1.60-2.04", "-3.64"
The program should be aimed specifically at teaching
the very basics of addition or multiplication of integers or decimals,
in a scaling difficulty.
Exercises that deviate from those specific concepts should be avoided.
Examples of what should NOT be implemented:
"2-4", # Deviates (slightly) since it also teaches that 2 == +2
"-2(+7)", # Deviates (same as example above)
"+2-4(-5)", # Deviates since it combines addition and multiplication
"""
import decimal
from random import randint, choice, random, seed
MAX_ABS_VALUE = 10
def rand_int_term(max_val=MAX_ABS_VALUE):
return randint(0, max_val)
def rand_float_term(max_val=MAX_ABS_VALUE):
return random() * max_val
def create_term(term_without_sign):
"""
Creates a single term.
:param term_without_sign:
:return: (num)
"""
sign = choice(['-', '+'])
if sign == '-':
term = -1 * term_without_sign
else:
term = term_without_sign
return term
DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = {
1: dict(
terms_count=2,
terms_type='int'
),
2: dict(
terms_count=3,
terms_type='int'
),
3: dict(
terms_count=2,
terms_type='float'
)
}
TOTAL_DIFFICULTY_LVLS = len(DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP)
class Terms(object):
def __init__(self, difficulty_lvl):
terms_data = DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP[difficulty_lvl]
self.terms_count = terms_data['terms_count']
self.terms_type = terms_data['terms_type']
def all_terms(self):
lst = []
if self.terms_type == 'int':
func = rand_int_term
elif self.terms_type == 'float':
func = rand_float_term
else:
raise NotImplemented('{}'.format(self.terms_type))
for _ in range(self.terms_count):
term_without_sign = func()
term = create_term(term_without_sign=term_without_sign)
lst.append(term)
return lst
class QuestionAndAnswer(object):
"""
Based on difficulty and operation type,
creates terms (either float or ints) which are then rounded to allow
specific number of decimals.
"""
OPERATIONS_TYPES = {'addition', 'multiplication'}
def __init__(self, difficulty_lvl, op_type):
self.terms_as_numbers = Terms(difficulty_lvl=difficulty_lvl).all_terms()
self.op_type = op_type
@staticmethod
def round_single_term_2_decimals(given_float):
num = decimal.Decimal(str(given_float))
return num.quantize(decimal.Decimal('.01'), decimal.ROUND_HALF_UP)
def terms_to_rounded_numbers(self):
"""
Ensures all terms have an appropriate number of decimals.
:return: (list)
"""
lst = []
for t in self.terms_as_numbers:
if isinstance(t, int):
lst.append(t)
else:
t = self.round_single_term_2_decimals(given_float=t)
lst.append(t)
return lst
def terms_as_strings(self):
lst = []
for t in self.terms_to_rounded_numbers():
if t > 0:
lst.append('+{}'.format(t))
elif t == 0:
random_sign = choice(['+', '-'])
lst.append('{sign}{term}'.format(sign=random_sign, term=t))
else:
lst.append(str(t))
return lst
def operation_str(self):
"""
Creates the operation string presented to the user.
:return: (str)
"""
terms_as_strings = self.terms_as_strings()
if self.op_type == 'addition':
op_str = ''.join(terms_as_strings)
elif self.op_type == 'multiplication':
op_str = ''
for t in terms_as_strings
Python has a style guide called PEP 8 which is definitly worth reading and and worth following if you do not have good reasons not to. In you case, your usage of empty lines for instance is not compliant to PEP8. You'll find tools online to check your code compliancy to PEP8 in a automated way if you want to.
Code organisation
You've written your code in classes which is sometimes a good thing. Sometimes, you don't need a class.
In any case, most of the things in the
Terms class could be extracted out of it (also removing the need for many self.). By the way, it may be interesting to point out that variables defined at the class levels can be pretty complicated and are not entirely needed and could be simple global variables.Also, you have variables that are not used at all and you should get rid of them.
Once everything than can be moved out of the class (and renamed) is moved out, you get:
```
"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)
Some examples of "questions" and their "answers":
"+2-4" , "-2"
"(-2)(+7)", "-14"
"-1.60-2.04", "-3.64"
The program should be aimed specifically at teaching
the very basics of addition or multiplication of integers or decimals,
in a scaling difficulty.
Exercises that deviate from those specific concepts should be avoided.
Examples of what should NOT be implemented:
"2-4", # Deviates (slightly) since it also teaches that 2 == +2
"-2(+7)", # Deviates (same as example above)
"+2-4(-5)", # Deviates since it combines addition and multiplication
"""
import decimal
from random import randint, choice, random, seed
MAX_ABS_VALUE = 10
def rand_int_term(max_val=MAX_ABS_VALUE):
return randint(0, max_val)
def rand_float_term(max_val=MAX_ABS_VALUE):
return random() * max_val
def create_term(term_without_sign):
"""
Creates a single term.
:param term_without_sign:
:return: (num)
"""
sign = choice(['-', '+'])
if sign == '-':
term = -1 * term_without_sign
else:
term = term_without_sign
return term
DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = {
1: dict(
terms_count=2,
terms_type='int'
),
2: dict(
terms_count=3,
terms_type='int'
),
3: dict(
terms_count=2,
terms_type='float'
)
}
TOTAL_DIFFICULTY_LVLS = len(DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP)
class Terms(object):
def __init__(self, difficulty_lvl):
terms_data = DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP[difficulty_lvl]
self.terms_count = terms_data['terms_count']
self.terms_type = terms_data['terms_type']
def all_terms(self):
lst = []
if self.terms_type == 'int':
func = rand_int_term
elif self.terms_type == 'float':
func = rand_float_term
else:
raise NotImplemented('{}'.format(self.terms_type))
for _ in range(self.terms_count):
term_without_sign = func()
term = create_term(term_without_sign=term_without_sign)
lst.append(term)
return lst
class QuestionAndAnswer(object):
"""
Based on difficulty and operation type,
creates terms (either float or ints) which are then rounded to allow
specific number of decimals.
"""
OPERATIONS_TYPES = {'addition', 'multiplication'}
def __init__(self, difficulty_lvl, op_type):
self.terms_as_numbers = Terms(difficulty_lvl=difficulty_lvl).all_terms()
self.op_type = op_type
@staticmethod
def round_single_term_2_decimals(given_float):
num = decimal.Decimal(str(given_float))
return num.quantize(decimal.Decimal('.01'), decimal.ROUND_HALF_UP)
def terms_to_rounded_numbers(self):
"""
Ensures all terms have an appropriate number of decimals.
:return: (list)
"""
lst = []
for t in self.terms_as_numbers:
if isinstance(t, int):
lst.append(t)
else:
t = self.round_single_term_2_decimals(given_float=t)
lst.append(t)
return lst
def terms_as_strings(self):
lst = []
for t in self.terms_to_rounded_numbers():
if t > 0:
lst.append('+{}'.format(t))
elif t == 0:
random_sign = choice(['+', '-'])
lst.append('{sign}{term}'.format(sign=random_sign, term=t))
else:
lst.append(str(t))
return lst
def operation_str(self):
"""
Creates the operation string presented to the user.
:return: (str)
"""
terms_as_strings = self.terms_as_strings()
if self.op_type == 'addition':
op_str = ''.join(terms_as_strings)
elif self.op_type == 'multiplication':
op_str = ''
for t in terms_as_strings
Code Snippets
"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)
Some examples of "questions" and their "answers":
"+2-4" , "-2"
"(-2)(+7)", "-14"
"-1.60-2.04", "-3.64"
The program should be aimed specifically at teaching
the very basics of addition or multiplication of integers or decimals,
in a scaling difficulty.
Exercises that deviate from those specific concepts should be avoided.
Examples of what should NOT be implemented:
"2-4", # Deviates (slightly) since it also teaches that 2 == +2
"-2(+7)", # Deviates (same as example above)
"+2-4(-5)", # Deviates since it combines addition and multiplication
"""
import decimal
from random import randint, choice, random, seed
MAX_ABS_VALUE = 10
def rand_int_term(max_val=MAX_ABS_VALUE):
return randint(0, max_val)
def rand_float_term(max_val=MAX_ABS_VALUE):
return random() * max_val
def create_term(term_without_sign):
"""
Creates a single term.
:param term_without_sign:
:return: (num)
"""
sign = choice(['-', '+'])
if sign == '-':
term = -1 * term_without_sign
else:
term = term_without_sign
return term
DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = {
1: dict(
terms_count=2,
terms_type='int'
),
2: dict(
terms_count=3,
terms_type='int'
),
3: dict(
terms_count=2,
terms_type='float'
)
}
TOTAL_DIFFICULTY_LVLS = len(DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP)
class Terms(object):
def __init__(self, difficulty_lvl):
terms_data = DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP[difficulty_lvl]
self.terms_count = terms_data['terms_count']
self.terms_type = terms_data['terms_type']
def all_terms(self):
lst = []
if self.terms_type == 'int':
func = rand_int_term
elif self.terms_type == 'float':
func = rand_float_term
else:
raise NotImplemented('{}'.format(self.terms_type))
for _ in range(self.terms_count):
term_without_sign = func()
term = create_term(term_without_sign=term_without_sign)
lst.append(term)
return lst
class QuestionAndAnswer(object):
"""
Based on difficulty and operation type,
creates terms (either float or ints) which are then rounded to allow
specific number of decimals.
"""
OPERATIONS_TYPES = {'addition', 'multiplication'}
def __init__(self, difficulty_lvl, op_type):
self.terms_as_numbers = Terms(difficulty_lvl=difficulty_lvl).all_terms()
self.op_type = op_type
@staticmethod
def round_single_term_2_decimals(given_float):
num = decimal.Decimal(str(given_float))
return num.quantize(decimal.Decimal('.01'), decimal.ROUND_HALF_UP)
def terms_to_rounded_numbers(self):
"""
Ensures all terms have an appropriate number of decimals.
DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = {
1: dict(
terms_count=2,
terms_type=rand_int_term
),
2: dict(
terms_count=3,
terms_type=rand_int_term
),
3: dict(
terms_count=2,
terms_type=rand_float_term
)
}
...
def all_terms(self):
lst = []
for _ in range(self.terms_count):
term_without_sign = self.terms_type()
term = create_term(term_without_sign=term_without_sign)
lst.append(term)def all_terms(self):
return [ create_term(term_without_sign=self.terms_type())
for _ in range(self.terms_count)]if isinstance(t, int):
lst.append(t)
else:
t = round_single_term_2_decimals(given_float=t)
lst.append(t)if not isinstance(t, int):
t = round_single_term_2_decimals(given_float=t)
lst.append(t)Context
StackExchange Code Review Q#135167, answer score: 5
Revisions (0)
No revisions yet.