Practical 11: Abstract Data Types and Custom Structures
Chapter Eighteen
Syllabus topic Module 2, "Exploring Abstract Data Types (ADT) and Custom Structures: Create and manipulate structures to model ADTs like Student, Book, or Employee. Implement basic operations (create, update, delete) using structures. Reflect on differences between primitive and abstract data types."
Pages 154 to 161 of 300
Aim
To model the abstract data types Student, Book and Employee as structures, to implement create, update and delete over a collection of them, and to state the difference between a primitive and an abstract data type.
What you need to know before you start
An abstract data type, or ADT, is a description of what a type does, with nothing said about how. It has two halves:
- The values it can hold. A Stack holds a sequence of items.
- The operations on it, with what each one takes, what it gives back, and what must be true
before and after. A Stack has push, pop, peek and is_empty, and pop on an empty Stack is an error.
That is all. An ADT does not say whether the Stack is an array or a linked list, and that is the point: the description is the contract, and the implementation is anybody's business. A program written against the contract keeps working when the implementation is replaced.
A data structure is the other half: a particular way of laying the values out in memory, with the operations written to suit it. So:
| Abstract data type | Data structure | |
|---|---|---|
| Says | what operations exist and what they mean | how the values are stored |
| Example | Stack, Queue, List, Tree, Map | array, linked list, hash table, heap |
| Written in | words, or a class with no bodies | code |
| Who cares | the person using it | the person implementing it |
| Can change without breaking callers | no | yes |
One ADT can have many structures. A Stack over an array and a Stack over a linked list are the same ADT and different data structures, and [Practical 15: the Stack ADT] builds both.
Primitive against abstract, which is MU's third bullet
| Primitive type | Abstract data type | |
|---|---|---|
| Examples | int, float, bool, char | Student, Book, Stack, Tree |
| Provided by | the language | you |
| How it is stored | the language decides, and it is one machine word | you decide |
| What you may do | a fixed set: add, compare, and so on | whatever operations you declare |
| Holds | one value | several values, and behaviour |
| Can it enforce a rule | no; an int will hold -5 marks | yes; a Student can refuse them |
That last row is the one to remember, and the whole reason this exercise exists. An int cannot protect itself. A type of your own can.
MU's three ADTs: Student, three ways
The same information, held three ways, so that the choice can be argued about rather than assumed.
# three ways to model one ADT
student_tuple = (2331, "Aarti Kulkarni", [78, 65, 92])
print("tuple :", student_tuple)
print(" the name is student_tuple[1]:", student_tuple[1])
student_dict = {"roll": 2331, "name": "Aarti Kulkarni", "marks": [78, 65, 92]}
print("dictionary :", student_dict)
print(" the name is student_dict['name']:", student_dict["name"])
class Student:
"""A student: the roll number, the name and three marks."""
def __init__(self, roll, name, marks):
self.roll = roll
self.name = name
self.marks = marks
def total(self):
return sum(self.marks)
def average(self):
return self.total() / len(self.marks)
def grade(self):
a = self.average()
if a >= 80:
return "distinction"
if a >= 60:
return "first class"
if a >= 40:
return "pass"
return "fail"
def __repr__(self):
return f"Student({self.roll}, {self.name!r}, {self.marks!r})"
s = Student(2331, "Aarti Kulkarni", [78, 65, 92])
print("class :", s)
print(" the name is s.name:", s.name)
print(" and it can compute: total", s.total(),
"average", f"{s.average():.2f}", "grade", s.grade())Practical 11: Abstract Data Types and Custom Structures
tuple : (2331, 'Aarti Kulkarni', [78, 65, 92])
the name is student_tuple[1]: Aarti Kulkarni
dictionary : {'roll': 2331, 'name': 'Aarti Kulkarni', 'marks': [78, 65, 92]}
the name is student_dict['name']: Aarti Kulkarni
class : Student(2331, 'Aarti Kulkarni', [78, 65, 92])
the name is s.name: Aarti Kulkarni
and it can compute: total 235 average 78.33 grade first classThe tuple is the smallest and the least readable. student_tuple[1] is the name, and nothing in the program says so. Change the order of the fields and every use of it is silently wrong. A tuple is also immutable: the marks list inside it can be changed, but the roll number cannot, which is sometimes exactly right and sometimes a nuisance.
The dictionary is readable and unguarded. student_dict["name"] says what it is, which is a real improvement. But nothing stops student_dict["nmae"] = "x" from quietly adding a fourth key, nothing requires a student to have marks at all, and the dictionary cannot compute anything: the total has to be worked out by whoever wants it, everywhere.
The class is the ADT. The fields have names, the operations live with the data, and the rules can be enforced. s.grade() is written once and used everywhere; sum(student_dict["marks"]) is written everywhere and gets changed in one place out of four.
Use a tuple for a value with two or three parts that never changes, such as a coordinate. Use a
dictionary when the keys are not known until the program runs, such as data read from a file.
Use a class when the thing has rules or behaviour, which in this module it always does.
Python has a shorthand that gives a class most of its boilerplate for nothing:
from dataclasses import dataclass
@dataclass
class Student:
roll: int
name: str
marks: listThat writes __init__, __repr__ and __eq__ for you. It is worth knowing and it is not used in this module, because the exercises are about writing the structure, and an examiner asking for a Student ADT wants to see __init__.
Practical 11: Abstract Data Types and Custom Structures
Create, update and delete, which need a collection
MU's second bullet asks for the basic operations, and they belong not to one object but to a collection of them. One Book cannot be created and deleted; a catalogue of Books can.
The four operations together are called CRUD: create, read, update, delete.
class Book:
"""A book in a library: the ADT and nothing but the ADT."""
def __init__(self, isbn, title, author, copies):
if copies < 0:
raise ValueError("a book cannot have a negative number of copies")
self.isbn = isbn
self.title = title
self.author = author
self.copies = copies
self.out = 0 # how many are lent out
def available(self):
return self.copies - self.out
def lend(self):
if self.available() == 0:
raise ValueError(f"no copy of {self.title!r} is available")
self.out += 1
def take_back(self):
if self.out == 0:
raise ValueError(f"no copy of {self.title!r} is out")
self.out -= 1
def __repr__(self):
return (f"Book({self.isbn!r}, {self.title!r}, {self.author!r}, "
f"{self.copies})")
class Library:
"""A collection of Books, with create, read, update and delete."""
def __init__(self):
self.books = {} # isbn -> Book
def create(self, isbn, title, author, copies):
if isbn in self.books:
raise KeyError(f"{isbn} is already in the catalogue")
self.books[isbn] = Book(isbn, title, author, copies)
return self.books[isbn]
def read(self, isbn):
if isbn not in self.books:
raise KeyError(f"{isbn} is not in the catalogue")
return self.books[isbn]
def update(self, isbn, **changes):
book = self.read(isbn)
for field, value in changes.items():
if not hasattr(book, field):
raise KeyError(f"a Book has no field called {field!r}")
setattr(book, field, value)
return book
def delete(self, isbn):
book = self.read(isbn)
if book.out:
raise ValueError(f"{book.title!r} has {book.out} copy out")
del self.books[isbn]
return book
def __len__(self):
return len(self.books)
def report(self):
print(f" {'isbn':<14}{'title':<32}{'copies':>7}{'out':>5}{'free':>6}")
for isbn in sorted(self.books):
b = self.books[isbn]
print(f" {b.isbn:<14}{b.title:<32}{b.copies:>7}{b.out:>5}"
f"{b.available():>6}")
print(f" {len(self)} title(s) in the catalogue")
lib = Library()
lib.create("978-0132126", "Operating System Concepts", "Silberschatz", 3)
lib.create("978-1118290", "Data Structures in Python", "Goodrich", 2)
lib.create("978-8183332", "Data Structures Through Python", "Kanetkar", 1)
print("after three creates")
lib.report()
print()
print("read : ", lib.read("978-1118290"))
lib.update("978-1118290", copies=5)
print("update: copies of Goodrich now", lib.read("978-1118290").copies)
lib.read("978-8183332").lend()
print("lend : Kanetkar out", lib.read("978-8183332").out,
"available", lib.read("978-8183332").available())
print()
for isbn, what in (("978-8183332", "delete a book that is lent out"),
("978-0000000", "read a book that is not there")):
try:
lib.delete(isbn)
except (KeyError, ValueError) as e:
print(f"{what}: {type(e).__name__}: {e}")
try:
lib.create("978-0132126", "Operating System Concepts", "Silberschatz", 1)
except KeyError as e:
print(f"create a book twice: KeyError: {e}")
try:
Book("978-1", "Nothing", "Nobody", -2)
except ValueError as e:
print(f"a negative number of copies: ValueError: {e}")
print()
lib.read("978-8183332").take_back()
lib.delete("978-8183332")
print("after taking the copy back and deleting")
lib.report()after three creates
isbn title copies out free
978-0132126 Operating System Concepts 3 0 3
978-1118290 Data Structures in Python 2 0 2
978-8183332 Data Structures Through Python 1 0 1
3 title(s) in the catalogue
read : Book('978-1118290', 'Data Structures in Python', 'Goodrich', 2)
update: copies of Goodrich now 5
lend : Kanetkar out 1 available 0
delete a book that is lent out: ValueError: 'Data Structures Through Python' has 1 copy out
read a book that is not there: KeyError: '978-0000000 is not in the catalogue'
create a book twice: KeyError: '978-0132126 is already in the catalogue'
a negative number of copies: ValueError: a book cannot have a negative number of copies
after taking the copy back and deleting
isbn title copies out free
978-0132126 Operating System Concepts 3 0 3
978-1118290 Data Structures in Python 5 0 5
2 title(s) in the cataloguePractical 11: Abstract Data Types and Custom Structures
What that program is careful about
Every operation that can fail, checks, and raises. There are five such checks and the output shows four of them firing:
| Operation | The check | The exception |
|---|---|---|
| create | the ISBN is already in the catalogue | KeyError |
| read | the ISBN is not in the catalogue | KeyError |
| update | a field name that a Book does not have | KeyError |
| delete | a copy of the book is lent out | ValueError |
| Book itself | a negative number of copies | ValueError |
The rule from [Python for Data Structures: the Tools This Module Uses] is being followed: a failed operation raises and does not return None. And the exception type carries meaning: KeyError for something not present, ValueError for something present and wrong.
update refuses a field that does not exist. hasattr(book, field) is the guard, and without it lib.update(isbn, copyes=5) would add a new attribute called copyes and the real copies would be untouched. The program would print no error and give the wrong answer, which is the dictionary's problem from the last section appearing in a class.
delete returns the book it removed. A delete that returns the deleted object lets a caller undo it, which is how the undo of [Practical 14: Doubly Linked Lists] works.
__len__ makes len(lib) work, which is the first of Python's protocols this module uses. A class that defines __len__ can be measured with len() and is also truthy when non-empty, so if lib: works. __repr__ and __len__ are the two every container in this module defines.
The catalogue is a dictionary keyed by ISBN, so read is one step whatever the size. A list of Books would have to be searched, which is O(n). That is a data structure decision inside the ADT, and no caller can tell: swap the dictionary for a hash table of your own from [Practical 20: Hashing and Collision Handling] and every line outside the class still works. That is what "abstract" means, demonstrated.
What abstraction actually buys
The Employee, MU's third named ADT, is where the benefit is easiest to see.
Practical 11: Abstract Data Types and Custom Structures
# a primitive type: what it is and what you may do with it are fixed
n = 42
print("an int:", n, "of type", type(n).__name__)
print(" everything it can do is built in:", n + 1, n * 2, n ** 2)
# an abstract data type: WHAT it does is declared, HOW is hidden
class Employee:
"""Name, basic pay and a rule for the allowance. The rule is hidden."""
HRA_RATE = 0.20
DA_RATE = 0.10
def __init__(self, name, basic):
self.name = name
self._basic = basic # the single underscore says: not yours
def basic(self):
return self._basic
def allowances(self):
return self._basic * (Employee.HRA_RATE + Employee.DA_RATE)
def gross(self):
return self._basic + self.allowances()
def raise_by(self, per_cent):
if per_cent < 0:
raise ValueError("a raise cannot be negative")
self._basic = self._basic * (1 + per_cent / 100)
def __repr__(self):
return f"Employee({self.name!r}, {self._basic:.2f})"
e = Employee("R. Deshmukh", 30000)
print()
print("an Employee:", e)
print(f" basic {e.basic():.2f}, allowances {e.allowances():.2f}, "
f"gross {e.gross():.2f}")
e.raise_by(10)
print("after a 10 per cent raise:", e)
print(f" gross is now {e.gross():.2f}")
print()
print("what makes it ABSTRACT:")
print(" the caller asks for gross() and never computes it")
print(" the two rates could become a table from a database")
print(" and not one line of the caller would change")an int: 42 of type int
everything it can do is built in: 43 84 1764
an Employee: Employee('R. Deshmukh', 30000.00)
basic 30000.00, allowances 9000.00, gross 39000.00
after a 10 per cent raise: Employee('R. Deshmukh', 33000.00)
gross is now 42900.00
what makes it ABSTRACT:
the caller asks for gross() and never computes it
the two rates could become a table from a database
and not one line of the caller would change_basic has one leading underscore. In Python that is a convention and not a lock: it means "this is not part of what I promise, do not touch it". Nothing stops a caller from writing e._basic = 0, and the underscore tells them that if they do, and it breaks later, that is their fault. Java would write private; Python writes an underscore and trusts you.
The caller never computes the gross pay. It asks. So when the allowance rule changes, and in India it changes every few years, one class changes and nothing else does. Had the caller been computing basic * 1.30 itself, in eleven places, the change would be eleven edits and one of them would be missed.
raise_by enforces a rule that an int cannot. A plain number for the basic pay would happily take a negative raise. The ADT refuses it.
Those three paragraphs are the answer to "what is the advantage of an abstract data type", and they are worth four marks:
- Encapsulation. The data and the operations on it are in one place, so a rule is written
Practical 11: Abstract Data Types and Custom Structures
once.
- Information hiding. The representation is private, so it can change without breaking
callers.
- A validated state. The type can refuse a value that makes no sense, which a primitive
cannot.
- Reuse. The type can be used anywhere without being re-understood.
Procedure
- Write the three Students. Run it, then change the order of the tuple's fields and note that
student_tuple[1] now gives the wrong thing with no error.
- Add a
passed()method to the class that returns True when every mark is 40 or more. - Write the Library. Run it and check all four error messages appear.
- Remove the
hasattrcheck fromupdate, calllib.update(isbn, copyes=9), and print the
catalogue. Nothing complains and nothing changed.
- Replace the dictionary in
Librarywith a list of Books and rewritereadto search it.
Confirm that no code outside the class changes.
- Write the Employee. Change
HRA_RATEto 0.25 and note that only one line moved. - Try
e._basic = -100and thene.gross(). The underscore is a convention, not a lock.
Result
The abstract data types Student, Book and Employee were modelled as classes. The Student was built as a tuple, a dictionary and a class and the three compared: only the class can name its fields, carry its own operations and enforce a rule. A Library of Books implemented create, read, update and delete over a collection, with five failure cases each raising the appropriate exception, and the representation inside it was shown to be replaceable without changing a line outside. The Employee showed the three benefits of abstraction: the allowance rule is written once, the representation is hidden behind an underscore, and a negative raise is refused.
Where marks are lost
- Saying an ADT is a class. A class is one way of implementing one. The ADT is the description
of the operations and their meanings.
- Not distinguishing the ADT from the data structure. A Stack is an ADT; an array is a data
structure; a Stack over an array is one implementation.
- Only the data, with no operations. A class with
__init__and nothing else is a record, not
an ADT.
- No validation. The whole answer to "why not just use an int" is that a type of your own can
refuse a wrong value.
- Returning
Nonefrom a failed create or delete instead of raising. - Using
KeyErrorandValueErrorinterchangeably. Not present is aKeyError; present and
wrong is a ValueError.
- No
__repr__, so the examiner sees a list of memory addresses. - Writing
updateso that a misspelled field silently creates a new one.
For the journal
Write the aim, MU's own wording, and the definitions of abstract data type and data structure in your own words, because the first question is always one of those. Then the Student three ways with its output, and one sentence on which you would use and why. Then the Library program in full with its run, including the four error lines, because the failure cases are what distinguish a complete answer. Then the Employee and the four advantages of abstraction as a list. The conclusion: an ADT declares what a type does and hides how it does it, which is what lets the representation change and what lets the type enforce rules a primitive cannot.
Practical 11: Abstract Data Types and Custom Structures
Quick revision
- An abstract data type is the values and the operations, with the implementation unspecified.
A data structure is a way of laying the values out. One ADT, many structures.
- Primitive types come from the language, hold one value, and cannot enforce a rule. An ADT is
yours, holds several values and behaviour, and can.
- Tuple: small, ordered, immutable, unnamed fields. Dictionary: named keys, no rules, no
behaviour. Class: named fields, its own operations, and it can refuse a bad value.
- CRUD is create, read, update, delete, and they belong to a collection, not to one object.
- A failed operation raises.
KeyErrorfor not present,ValueErrorfor present and wrong. updatechecks withhasattrthat the field exists, or a misspelling silently adds a new one.- A leading underscore means "not part of the promise". It is a convention, not a lock.
__repr__for a readable printout,__len__so thatlen()and truthiness work.- The four advantages: encapsulation, information hiding, a validated state, and reuse.
- The Library's catalogue is a dictionary, and could be a list or a hash table, and no caller
would know. That is abstraction in one sentence.
Questions you should be able to answer
1. Define an abstract data type. A description of a type by the values it can hold and the operations on it, with what each operation takes, gives and requires, and with nothing said about how the values are stored.
2. What is the difference between an ADT and a data structure? The ADT is the contract: what the operations are and what they mean. The data structure is the implementation: how the values are laid out in memory. One ADT can be implemented by several structures.
3. Give three differences between a primitive type and an abstract data type. A primitive comes from the language and an ADT from you; a primitive holds one value and an ADT holds several plus behaviour; a primitive cannot refuse a meaningless value and an ADT can.
4. Why model a Student as a class rather than a dictionary? Because the class names its fields, so a misspelling is an error rather than a new key; it carries its own operations, so the total and the grade are written once; and it can enforce rules, such as refusing a negative mark.
Practical 11: Abstract Data Types and Custom Structures
5. What are the four basic operations on a collection, and why do they not belong to one object? Create, read, update and delete. One Book cannot be created or deleted; the catalogue that holds it can.
6. lib.update(isbn, copyes=5) is called by mistake. What should happen, and what happens without a guard? It should raise a KeyError naming the unknown field. Without the hasattr guard it adds a new attribute called copyes, changes nothing that matters, and reports no error.
7. What does a single leading underscore on an attribute mean in Python? That it is not part of the class's promise and a caller should not use it. It is a convention: nothing prevents access, and a caller who relies on it has no complaint when it changes.
8. The Library keeps its books in a dictionary. What would change if it kept them in a list? Inside the class, read would have to search, so it would go from one step to O(n). Outside the class, nothing at all. That is what information hiding means.
9. Name the four advantages of using an abstract data type. Encapsulation, so the data and its operations are together; information hiding, so the representation can change; a validated state, so a meaningless value can be refused; and reuse, because the type can be used without being re-understood.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.