COMP 202 Foundations of Programming • McGill University, Montreal
Corrected exercises: classes and objects, COMP 202 at McGill
This is the corrected exercise set on classes and objects for COMP 202, Foundations of Programming, the first Python course taken at McGill University. It is the last chapter of the course and the one the final assignment is usually built on. Part A covers the mechanics: the class and its instances, self, instance against class attributes, the methods Python calls on your behalf, encapsulation and validation, and objects passed to functions. Part B works at assignment level: a bank account with an invariant, a list of objects, five statements to correct, composition, and inheritance.
The thread running through the set: an object bundles a STATE with the operations allowed on that state, and the whole value of a class is the promise it can then keep. A balance that is never negative, a list of marks that holds only valid marks, a student that always has the same fields: none of that can be guaranteed by a dictionary of records, and all of it costs five lines in a class. self is simply the object the method was called on, and reading ana.average() as Student.average(ana) makes every question of the chapter easy.
The traps named explicitly in the solutions: a method header written without self, a mutable attribute placed in the class body and shared by every object, self.count = self.count + 1 creating an instance attribute that hides the class one, a __str__ that prints instead of returning, an equality that compares identity because __eq__ was never defined, sorted refusing objects with no key, a child __init__ that forgets super, and a subclass written for two objects that merely share a word.
10 corrected exercises • 100 points
• 150 minutes
Course recap
•A class is written once; objects are created at run time, each with its own state.
•self is the object the method was called on: read ana.average() as Student.average(ana).
•Every method takes self as its FIRST parameter, including the ones that seem to need nothing.
•__init__ initialises an object that Python has already created. It is the one place where the whole state is declared.
•Reading an attribute falls back to the class; ASSIGNING one always creates it on the object.
•A mutable attribute in the class body is shared by every object, exactly like a mutable default argument.
•print(obj) calls __str__, which must RETURN a string. Without it, Python shows the class name and an address.
•Without __eq__, the equality sign compares identity: two objects built separately are never equal.
•sorted on objects needs a key, or a __lt__ defined on the class.
•An object passed to a function is not copied: mutating it is visible to the caller, rebinding the parameter is not.
•Inheritance is for IS A, composition is for HAS A. A child __init__ calls super().__init__() first.
Part A: the mechanics (/50)
Exercise 1: A class is a blueprint, and self is the object
Up to here a program has kept its data in lists and dictionaries and its behaviour in functions, with nothing tying the two together. A class ties them: it declares what an object of that kind KNOWS, its attributes, and what it can DO, its methods, and every object built from it carries its own copy of the state.
The figure shows one class and two objects. The class exists once and is written once; the objects are created at run time, as many as needed, each with its own name and its own marks.
a) Write the class Student with a name and a list of marks, and create two objects from it.
b) What is self, and who supplies it? Answer for the call ana.average().
c) The header is written def average(): with no self. Give the error raised at the call, and its wording.
d) The attributes are created outside __init__, in the method that first needs them. Give two things that break.
e) Student is written with no __init__ at all. Is the class usable, and what does that say about what __init__ is for?
Show the solution
a) class Student: then def __init__(self, name): then self.name = name and self.marks = []. The objects: ana = Student('Ana') and ben = Student('Ben'). The call does not mention self, Python supplies it, and __init__ is not called by name: writing Student('Ana') creates the object and runs __init__ on it. Note that self.marks = [] must be inside __init__, so that each student gets a fresh list, for the same reason as the mutable default argument of the functions set.
b) self is the OBJECT the method was called on, and Python supplies it automatically from what stands to the left of the dot. In ana.average(), self is ana, so self.name is 'Ana' inside that call. The word itself is a convention rather than a keyword, and using another name works and costs style marks. The habit that makes the whole chapter easy: read ana.average() as Student.average(ana), which is literally what Python does.
c) TypeError, average() takes 0 positional arguments but 1 was given. The message is confusing at first sight because the call ana.average() looks as though it passes nothing, and it is exactly right: the object is passed as the first argument, and the header did not declare a parameter for it. Every method of a class takes self as its first parameter, including the ones that seem to need nothing.
d) First, an object can exist without them, so a method that reads self.marks raises AttributeError on an object whose other method has not run yet. Second, nobody can tell what a Student is by reading one place: the state is scattered over the file, and the class no longer documents itself. __init__ is the one place where the full state of an object is declared, and keeping it complete is what makes the rest of the class safe to write.
e) It is usable: objects can be created, and attributes can be attached to them from outside, ana.name = 'Ana'. It is also useless as a class, because nothing guarantees that two Student objects have the same attributes, and any method may find a missing one. That is what __init__ is for: it does not create the object, Python does that, it INITIALISES it, and its real job is to guarantee that every object of the class starts complete.
Exercise 2: Instance attributes, class attributes, and the one everybody shares
An attribute written with self belongs to ONE object. An attribute written in the body of the class, outside every method, belongs to the class itself and is shared by every object of it. The distinction matters as soon as something must be counted across all the objects, and it bites in exactly the same way as the mutable default argument.
The table gives the five forms and where each one puts its value.
a) Add to Student a counter of how many students have been created. Where is it declared, and where is it increased?
b) The counter is increased with self.count = self.count + 1. Trace what happens on the first two objects, and give the value the class attribute ends up with.
c) A class attribute is written marks = [] in the class body, and __init__ does not touch it. Describe what two students share, and connect it to a trap of the functions set.
d) An attribute is added to one object after it was created, ana.year = 2. Is that legal, and what does a reader lose?
e) State the rule that decides between an instance attribute and a class attribute, and give one honest use of each.
Show the solution
a) Declared in the class body, count = 0, outside every method, and increased inside __init__ with Student.count = Student.count + 1. Naming the CLASS on the left is what makes the increment act on the shared value; that is also why the line reads oddly at first, since everything else in __init__ is written with self.
b) Reading self.count works, because a name not found on the object is looked up on the class, so the first object reads 0. ASSIGNING self.count creates a brand new attribute ON THAT OBJECT with the value 1, and the class attribute is untouched. The second object does the same and also ends at 1. The class attribute stays 0 for ever, and each object claims to be the first. The rule to keep: reading falls back to the class, assigning never does.
c) The two students share ONE list, so a mark appended for Ana appears in Ben's marks as well, and the class average is computed twice over the same data. It is the mutable default argument again, in another costume: a mutable object created once, at definition time, and reached by everything that comes later. The repair is the same, create it per object, self.marks = [] inside __init__.
d) It is legal, since Python attaches attributes on demand. What a reader loses is the guarantee: from that line on, some Student objects have a year and others do not, and any method that reads self.year works on one object and raises AttributeError on the next. It also defeats the point of __init__ as the one place where the state is declared. Adding an attribute after the fact is a debugging convenience, never a design.
e) The rule: does the value belong to ONE object or to the KIND? A mark, a name and a balance belong to the object, so they are instance attributes. A count of the objects created, a constant such as a passing grade of 60, or a default interest rate belong to the kind, so they are class attributes. The honest uses are exactly those: shared constants and shared counters, and everything mutable per object goes through self.
Exercise 3: The methods Python calls for you
Some methods are never called by name. print looks for __str__, the equality sign looks for __eq__, len looks for __len__. Defining them is what makes an object of your own behave like the built in types, and not defining them is why print shows an address and two identical students compare as different.
The table pairs what is written with what Python actually calls, and says what happens when the method is missing.
a) print(ana) shows something like an object at 0x7f9c1d2b. Explain what it is, and write the __str__ that fixes it.
b) Why must __str__ RETURN the string rather than print it? Give what breaks otherwise.
c) Two Student objects have the same name and the same marks. What does the equality sign answer by default, and why?
d) Write __eq__ so that two students with the same name are equal. Name one consequence for the in operator.
e) A list of Student objects is passed to sorted with no key. Give the error, and the two ways of sorting them by average.
Show the solution
a) It is the default text representation: the class name and the memory address of the object, which is all Python can say about an object whose class has not said anything. The repair: def __str__(self): return self.name + ', average ' + f'{self.average():.1f}'. From then on print(ana) shows that line, and so does str(ana) and an f-string containing the object.
b) Because print calls it and prints WHAT IT RETURNS. A __str__ that prints instead of returning prints its text at the wrong moment, returns None, and then print raises TypeError, __str__ returned non-string. It is the print against return distinction of the functions set, in the one place where the language checks it and says so.
c) It answers False. Without __eq__, the equality sign on two objects compares IDENTITY: it asks whether the two names refer to the same object in memory, not whether they hold the same values. Two students built by two calls to Student are two objects, so they differ. That is the right default, since Python cannot guess which attributes make two objects the same.
d) def __eq__(self, other): return self.name == other.name. The consequence for in: the operator uses equality, so ana in students is then True for any student of the same name, and list.remove and index follow the same rule. That is convenient and it is also a promise: two objects that compare equal should behave the same everywhere, so an __eq__ that ignores the marks says the name IS the identity of a student.
e) TypeError, less than not supported between instances of Student. sorted needs to compare, and comparison is __lt__, which the class does not define. The two ways: give sorted a key, sorted(students, key=lambda s: s.average()), which is the one to prefer since it says on what the order is based; or define __lt__ on the class, which makes the order a property of the type and is right only when there is one obvious order.
Exercise 4: What the class promises: validation and invariants
The reason to put the state and the operations in the same place is that the class can then GUARANTEE something about that state. A balance is never negative; a list of marks holds only numbers between 0 and 100; a date is a real date. Such a promise is called an invariant, and it holds only if every path into the state goes through the class.
Python has no private attributes. It has a convention, a leading underscore, which says do not touch this from outside, and it is enforced by readers rather than by the interpreter.
a) Give the invariant of a Student holding marks out of 100, and the line of __init__ that establishes it.
b) A method add_mark(self, m) is written. Give its validation and say what it should do with 130.
c) The caller writes ana.marks.append(130) directly. Is the invariant still guaranteed? What does the underscore convention change?
d) Write a method that returns the average and explain why it is a method rather than an attribute computed in __init__.
e) Give the case where a stored attribute IS the right answer rather than a computed one, and what must then be maintained.
Show the solution
a) The invariant: self.marks is a list of numbers, each between 0 and 100 inclusive. __init__ establishes it in the simplest possible way, self.marks = [], since an empty list satisfies it, and every later change goes through the class. Starting from an empty state is what makes an invariant easy to establish, and it is also why a constructor that accepts a list from outside must validate it.
b) if not (0 <= m <= 100): raise ValueError with a message naming the value; otherwise self.marks.append(m). With 130 it must RAISE, not ignore and not clamp to 100: the caller passed something impossible and needs to know, whereas a silent clamp would put a mark in the list that nobody ever gave. This is the functions set rule again, raise rather than return a special value.
c) It is not guaranteed. Nothing in Python stops the caller from reaching into the list and appending anything, and the class cannot notice. The underscore convention, self._marks, changes nothing technically and everything socially: it says this is the inside of the class, and a reader who writes ana._marks.append(130) knows they are breaking a promise. Real protection would mean returning a COPY from the accessor, which costs a copy each time and is rarely worth it in a first course.
d) def average(self): if self.marks == []: return None, then return sum(self.marks) / len(self.marks). It is a method because it is DERIVED from the state and must follow it: an attribute computed once in __init__ would be the average of an empty list at that moment and would then be wrong after the first mark is added. The general rule: store what is independent, compute what follows from it.
e) A stored attribute is right when the computation is expensive and the state changes rarely, for instance a total recomputed over ten thousand marks in a program that reads them once. What must then be maintained is the agreement between the store and the state: every method that changes the marks must update the total, and that duty is exactly what makes a stored derived value a source of bugs. The compromise is to keep it inside the class, so that only the class can get it wrong.
Exercise 5: An object handed to a function
An object is passed to a function the same way a list is: the function receives the object itself, not a copy. So a method or a function that changes an attribute changes it for the caller too, and the design question of the functions set comes back unchanged, namely does this operation return something new or modify what it was given.
The difference with a list is that a class can DECIDE, and say so in its docstring.
a) A function bonus(student) does student.marks.append(10). Does the caller see it? And if the function does student = Student('copy')?
b) Write two versions of a raise operation on an Account: one that changes the balance, one that returns a new Account. Give the docstring line of each.
c) Two names refer to the same Student object. Give the line that tells them apart from two students with equal attributes.
d) A copy of an object is needed. Give the module and the call, and say what a shallow copy of a Student shares.
e) State when a method should return self, and give the risk that comes with it.
Show the solution
a) Yes, the caller sees the appended mark: the parameter and the caller's variable name the same object, and appending changes that object. No, the caller does not see the reassignment: student = Student('copy') rebinds the LOCAL name inside the function and leaves the caller's object alone. It is exactly the list story of the foundations set, and the rule is the same, mutating is visible, rebinding is not.
b) In place: def add_interest(self, rate): self.balance = self.balance * (1 + rate), documented as changes the balance of this account and returns None. Pure: def with_interest(self, rate): return Account(self.owner, self.balance * (1 + rate)), documented as returns a NEW account, this one is unchanged. The two names are different on purpose, since a reader who sees the result of the first assigned to something has spotted a bug.
c) a is b, which asks whether the two names refer to the same OBJECT, while a == b asks whether they are equal in the sense the class defined. Two students built separately with the same name are equal under the __eq__ of exercise 3 and are not the same object, so is answers False and the equality sign answers True. That pair of questions is the same one the copying exercise of the structures set asked about lists.
d) import copy, then twin = copy.copy(student) for a shallow copy or copy.deepcopy(student) for a deep one. A shallow copy of a Student shares the LIST of marks: the two objects have different name attributes and the same list object, so a mark added through one appears in the other. It is the slice copy of a nested list, one level up, and the answer is the same, deepcopy when the attributes are mutable.
e) A method should return self when the class is designed for chained calls, report.add(x).add(y).sort(). The risk is that it then looks like a pure method: a reader who writes new = old.add(x) believes old is unchanged and it is not, since what came back IS old. Python's own convention avoids this by returning None from mutating methods, and a first course is better off following it.
Part B: problems and reasoning (/50)
Exercise 6: An account that keeps its promise
A bank account is the standard first class because its invariant is easy to state and impossible to keep without one: the balance is never negative, and it changes only through a deposit or a withdrawal.
Everything asked here has been met separately in the earlier sets. What is new is that the guarantees now live in one place instead of being repeated at every call site.
a) Write Account with an owner and a balance, its __init__ and its __str__.
b) Write deposit and withdraw with their validation. What does each one return, and why?
c) An account holds 100 dollars. Follow deposit(50), withdraw(30) and withdraw(200), giving the balance after each.
d) The balance is stored as a float. Give the problem with money, and the standard repair.
e) A caller writes account.balance = -500. Explain how it got past the validation, and give the two levels of answer.
Show the solution
a) class Account: then def __init__(self, owner, balance=0): with self.owner = owner and self.balance = balance, plus a validation that the starting balance is not negative. The __str__: def __str__(self): return f'{self.owner}: {self.balance:.2f} dollars'. The default of 0 is immutable, so it is a safe default, unlike a list.
b) def deposit(self, amount): if amount <= 0: raise ValueError, else self.balance = self.balance + amount. def withdraw(self, amount): if amount <= 0: raise ValueError, then if amount > self.balance: raise ValueError with a message naming the balance, else self.balance = self.balance - amount. Both return None, because both CHANGE the object, and Python's convention is that a mutating call returns nothing. Returning the new balance is defensible and must then be documented.
c) After deposit(50) the balance is 150. After withdraw(30) it is 120. withdraw(200) raises ValueError and the balance stays at 120: the check happens BEFORE the subtraction, so a refused operation leaves the object exactly as it was. That last point is the invariant doing its work, and it is what makes the class safe to call from code that does not check anything itself.
d) A float cannot hold most decimal amounts exactly, so a long series of deposits and withdrawals drifts, and two balances that should be equal compare as different, exactly as in the very first set. The standard repair is to store CENTS as an integer, and to divide by 100 only when displaying. The alternative is the decimal module, which is what real financial code uses.
e) It got past because the attribute is public and Python has no way to intercept a plain assignment unless the class asks for one. The two levels of answer: by convention, name it self._balance and provide a read only accessor, which tells every reader that assigning to it is a breach; by mechanism, use a property, which lets the class run code on assignment and raise. In COMP 202 the convention is the expected answer, and knowing that the mechanism exists is worth a sentence.
Exercise 7: A list of objects, and what it replaces
The structures set stored a student as a dictionary of fields inside a dictionary keyed by name. The same data can be a list of objects, and the two are not equivalent in what they guarantee.
The comparison is the point of this exercise: a dictionary of records is quicker to write and promises nothing; a class costs five lines and then promises that every record has the same fields and that they are valid.
a) Build a list of three Student objects, then print the name of each with a loop.
b) Sort the list by average, highest first. Give the call, and say what the list holds afterwards.
c) Compute the class average from the list. Give the one line, and the guard it needs.
d) Find the students above the class average. Give the comprehension, and say how many passes over the list it costs.
e) Give two things the class version guarantees that the dictionary of records of the structures set does not.
Show the solution
a) students = [Student('Ana'), Student('Ben'), Student('Chen')], with the marks added afterwards through add_mark, then for s in students: print(s.name). Note that printing s itself would use __str__ and show the whole line, which is usually what a report wants; s.name is the version that says only the name.
b) sorted(students, key=lambda s: s.average(), reverse=True). It returns a NEW list holding the same objects in a different order: the objects are not copied, so a change made through one list is visible through the other, which is right, since there is only one Ana. students.sort(key=...) reorders the list in place instead and returns None, exactly as in the structures set.
c) sum([s.average() for s in students]) / len(students), with the guard if students == []: before it, since an empty list would give ZeroDivisionError. There is a second guard hiding here: a student with no marks returns None from average, and None cannot be summed, so either the class returns 0 for an empty student or the comprehension filters those out. Deciding that in one place is precisely what the class is for.
d) mean = the class average, then [s for s in students if s.average() > mean]. It costs TWO passes, one to compute the mean and one to select, and that is unavoidable: nothing can be compared to the mean before the mean is known. It is worth stating because the instinct is to try to do it in one pass, and the honest answer is that a two pass algorithm is the right one here.
e) First, that every student has the same fields, because __init__ creates them all: a dictionary of records grows a field the day someone writes a new key by mistake, and the typo creates it silently. Second, that the values are valid, because add_mark refuses anything outside the range, whereas nothing stops a dictionary from receiving a mark of 130. A third, in passing: the class gives the objects a printed form and an equality, which a raw dictionary can only imitate.
Exercise 8: Five statements to correct
Each of the five statements below is plausible, has been written by a student in an examination, and is false. For each one, say what is wrong, give the counterexample that settles it, and write the correct version.
The marks go to the counterexample: a class, a call, and what really happens.
a) 'self is a keyword of Python, like def or return.'
b) 'An attribute written in the class body is created once per object, like one written in __init__.'
c) 'Two objects with the same attribute values are equal, so the equality sign returns True.'
d) '__str__ prints the object, which is why print(obj) works.'
e) 'A subclass must redefine every method of its parent, otherwise it does not have them.'
Show the solution
a) False. It is an ordinary parameter name, and the only rule is that it is the FIRST parameter of a method, filled in by Python with the object to the left of the dot. The counterexample: a method written def average(this): works exactly like one written with self, and Python says nothing. Correct version: self is a universal convention, worth following without exception, and it is not enforced by the language.
b) False. An attribute in the class body is created ONCE, for the class, and shared by every object. The counterexample: with marks = [] in the class body, ana.marks.append(8) makes ben.marks show 8 as well. Correct version: per object state goes in __init__ with self; the class body is for constants and counters shared by the kind.
c) False by default. Without __eq__, the equality sign compares identity, so two objects built separately are different whatever they hold. The counterexample: Student('Ana') == Student('Ana') is False. Correct version: define __eq__ to say which attributes make two objects equal, and only then does the equality sign, and with it the in operator, answer on the values.
d) False. __str__ RETURNS a string and prints nothing; print is what prints, using the returned string. The counterexample: a __str__ that calls print instead of returning makes print(obj) show the text at the wrong moment and then raise TypeError, __str__ returned non-string. Correct version: __str__ returns the text, print displays it.
e) False, and it is the point of inheriting. A subclass has every method of its parent without writing anything, and it redefines only the ones whose behaviour must change. The counterexample: a SavingsAccount that defines only add_interest can still be deposited into and withdrawn from. Correct version: a subclass inherits everything and overrides what it needs, calling the parent's version with super when it wants to extend rather than replace.
Exercise 9: A course made of students
An attribute may be an object of another class, or a list of them. That is composition, and it is the relation almost every real program is built on: a course HAS students, an order HAS lines, a board HAS squares.
The rule that keeps it readable: each class answers only about itself, and asks the objects it holds for the rest.
a) Write Course with a title and a list of students, plus the method that adds one.
b) Write Course.average(), and say which class computes what.
c) The Course is asked for the name of its best student. Write the method, and give what it returns for an empty course.
d) A Course method reaches inside a student and writes student.marks.append(x). Say why that is a design mistake, and give the repair.
e) Write Course.__str__ so that printing a course shows one line per student. Which method does it call, and what does that show about composition?
Show the solution
a) class Course: then def __init__(self, title): with self.title = title and self.students = [], then def add_student(self, student): self.students.append(student). The list is created per object, inside __init__, for the reason of exercise 2 c). Note that add_student takes a Student OBJECT, not a name: building the student is the caller's job, and a course that built its own students would be doing two jobs.
b) def average(self): if self.students == []: return None, then return sum([s.average() for s in self.students]) / len(self.students). The Student computes its own average, since it is the only class that knows its marks; the Course averages those numbers, since it is the only one that knows its students. Each class answers about what it holds, and nothing reaches through the other's state to recompute what that class already provides.
c) def best(self): if self.students == []: return None, then return max(self.students, key=lambda s: s.average()).name. On an empty course it returns None, which is the honest answer, and the caller has to handle it; raising a ValueError would also be defensible and must be documented. Note that max on an empty sequence raises by itself, so the guard is not decoration.
d) It breaks the guarantee of the Student class: the validation that refuses a mark of 130 lives in Student.add_mark, and a Course that appends directly walks around it. It also ties the two classes together, so that a change to how a Student stores its marks breaks the Course. The repair is one word, call the method: student.add_mark(x). A class should talk to the objects it holds through their methods, exactly as an outside caller does.
e) def __str__(self): lines = [self.title] then for s in self.students: lines.append(' ' + str(s)), then return the pieces joined with newlines. It calls str on each student, which calls Student.__str__: the course describes itself by asking each part to describe itself. That is composition in one line of code, and it is why adding a field to a Student changes the course report with nothing to edit in Course.
Exercise 10: Inheritance, and when not to use it
A subclass declares that its objects ARE objects of the parent class, with something added or changed. Everything the parent defines is available without being rewritten, and a method redefined in the child replaces the parent's for objects of the child.
The relation to test before writing it is the word IS. A savings account IS an account. A course is NOT a student, however many students it holds, which is why the previous exercise used composition.
a) Write SavingsAccount as a subclass of Account, adding a rate and a method add_interest.
b) Its __init__ needs the owner and the balance as well. Give the line that reuses the parent's work, and say what happens without it.
c) SavingsAccount redefines __str__. Which one runs for a SavingsAccount, and which for an Account?
d) An account of 1000 dollars at 2 percent. Give the balance after add_interest, and after a withdrawal of 500.
e) A programmer makes Course a subclass of Student, on the grounds that both have a name and an average. Say what is wrong, and what the right relation is.
Show the solution
a) class SavingsAccount(Account): then def __init__(self, owner, balance=0, rate=0.02): with the parent's initialisation and self.rate = rate; then def add_interest(self): self.balance = self.balance * (1 + self.rate). deposit, withdraw and __str__ are not written at all, and they work, because the child has everything the parent defined.
b) super().__init__(owner, balance), as the first line of the child's __init__. Without it the parent's initialisation never runs, so self.owner and self.balance are never created, and the first call to deposit raises AttributeError, SavingsAccount object has no attribute balance. Copying the two assignments into the child instead of calling super also works and duplicates the validation, which is the same argument as the average written twice in the functions set.
c) The child's version runs for a SavingsAccount and the parent's for an Account: Python looks for the method on the object's own class first, and only then on its parent. That is what overriding means. A child that wants to EXTEND rather than replace calls the parent's version inside its own with super().__str__(), and adds to it.
d) After add_interest the balance is 1000×1,02=1020 dollars. After withdraw(500) it is 520 dollars, and the withdrawal is checked by the PARENT's method, which the child inherited unchanged, so the guarantee that the balance never goes negative still holds for the child. Inheriting an invariant along with the methods is the real benefit here.
e) The test fails: a Course is not a Student, and no code that expects a Student would work if handed a Course. Sharing two attribute names is not a reason to inherit; it is a coincidence of vocabulary. The right relation is the one of exercise 9, composition: a Course HAS a list of Students. The rule to carry away is that inheritance is for IS A, composition is for HAS A, and when in doubt in a first course, composition is almost always the correct answer.