Dataclasses and Value Objects · capstone
Capstone project: Project: Model Result Records and Configuration
This chapter’s whole
The project is a small results system where every choice from the chapter appears once.
What you are building
QuizConfig how a quiz runs changeable, has defaults
Points an amount frozen value
Question a question frozen value, built several ways
ResultRecord one finished result frozen value
Gradebook the collection entity, changes as results arrive
Three frozen values, one changeable configuration, and one entity that exists in order to grow.
The decisions worth pausing on
Why ResultRecord is frozen. A finished result is a fact about something that already happened. Nothing should be able to edit last week’s score, and freezing says so in a way a comment cannot.
Why Gradebook is not. It exists to accumulate. Freezing it would mean rebuilding the whole gradebook on every result. eq=False also preserves entity identity: two separate gradebooks do not become the same
Why Gradebook holds a list built per object. Lesson 4 and Lesson 8 both showed the same bug arriving by different doors: a Gradebook per program is the common case, which is exactly why sharing a list between two of them would go unnoticed for a long time.
Why is ResultRecord frozen while Gradebook is not?
The methods that are not boilerplate
@dataclass writes the constructors, reprs, and comparisons. What is left is yours, and it is what the grader spends most of its time on:
Question.from_dictandQuestion.simple, two more ways in;Question.normalize, the helper that belongs to questions;Question.is_correctandpoints_for, the rules a question owns;ResultRecord.percentageandpassed_under, facts about a result;Gradebook.record,best, andaverage, comparing normalized percentages so results with different totals stay meaningful.
Not one of those could have been generated, because each one is a decision somebody made.
Finishing
Count the lines you did not have to write. Then read the ones you did, and check that every one of them says something that could not have been worked out from the list of attributes above it.
Add two results with different totals, such as 9/100 and 8/10, and print the best result plus the average percentage. That final run is the proof that the gradebook models results rather than merely passing the supplied examples.
Task
Build the five classes, using the generated machinery for everything it covers.
Points is a frozen amount, with plus(other) returning a new Points.
Question is a frozen, validated value holding non-empty prompt and answer and points defaulting to Points(1).
QuizConfig is a changeable dataclass: a required non-empty title, pass_mark defaulting to 0.6 and constrained above 0 and at most 1, and tags starting as a new empty
ResultRecord is a frozen value holding a non-empty learner, non-negative total, and score from 0 through total, with percentage and a passed_under(config)
Gradebook is a changeable entity holding a non-empty title and one internal _records list. Use eq=False. It gets record(result), records() returning a safe copy, best(), and average().
Complete the methods named in the starter:
Question.from_dict(record)readsprompt,answer, and aPointsvalue frompoints;simple(prompt, answer)usesPoints(1). Both are class methods constructing throughcls. Reject a non-Pointspointswith . Question.normalize(text)is a static method that strips and lowercases text. Use it on both sides inis_correct.points_forreturns the integer amount for a correct answer and0otherwise.QuizConfig.practice(title)andfinal_exam(title)are class methods choosing pass marks of0.5and0.8respectively; tags still start empty.ResultRecord.percentageis the fractionscore / total, or0.0for a zero total.passed_under(config)includes a result exactly on the config’s pass mark.Gradebook.recordaccepts onlyResultRecordvalues, raisingTypeErrorotherwise.best()compares percentages and returns a highest result, orNonewhen empty.average()is the mean of the recorded percentages, or0.0when empty.
Use ValueError for the invalid field values described above. Keep the gradebook history out of constructor arguments and its generated repr with init=False, repr=False, as in Lesson 7. No class prints.