Classes and objects are two of programming’s power tools. They let good programmers accomplish a great deal in very little time, but with them, bad programmers can create a real mess. This section introduces some underlying theory that will help you design reliable and reusable object-oriented software.
To encapsulate something means to enclose it within a container. In programming, encapsulation refers to keeping data and the code that uses it in one place, while hiding the details of how they interact. For example, each instance of a fictitious class file keeps track of disk reading and writing operations, as well as the current position within the file. The class conceals the implementation details, allowing programmers to use it without needing to understand the underlying mechanisms.
Classes support polymorphism: if two classes have methods that work the same way, instances of those classes can replace one another
Polymorphism means “having more than one form.” In programming, it means that an expression involving a variable can perform differently depending on the type of object to which the variable refers. For example, if obj refers to a string, then obj[1:3] produces a two-character string. If obj refers to a list, on the other hand, the same expression produces a two-element list. Similarly, the expression left + right can produce a number, a string, or a list, depending on the types of left and right.
Polymorphism is widely used in modern programming to reduce the amount of code that programmers need to write and test. For example, it allows you to write a generic function to count non-blank lines:
| | def non_blank_lines(thing): |
| | """Return the number of nonblank lines in thing.""" |
| | |
| | count = 0 |
| | for line in thing: |
| | if line.strip(): |
| | count += 1 |
| | return count |
You can apply this function to a list of strings, a file, or a web document halfway around the world (see ) or in the cloud. Each of those three types knows how to be the subject of a loop; in other words, each one knows how to produce its “next” element as long as there is one and then say “all done.” That means that instead of writing four functions to count interesting lines or copying the lines into a list and then applying one function to that list, you can apply one function to all those types directly.
New classes can be defined by inheriting features from existing ones
Implementing the same methods in several classes is one way to make them polymorphic. Still, it suffers from the same flaw as initializing an object’s instance variables from outside the object. If a programmer forgets just one line of code, the whole program can fail for reasons that will be difficult to track down. A better approach is to utilize a third fundamental feature of object-oriented programming called inheritance, which enables you to reuse code more effectively.
Whenever you create a class, you are using inheritance: your new class automatically inherits all of the attributes of the class object, much like a child inherits attributes from their parents. You can also declare that your new class is a subclass of some other class.
Here is an example. Let’s say you’re managing people at a university. There are students and faculty. (This is a gross oversimplification for purposes of illustrating inheritance; you’re ignoring administrative staff, caretakers, food providers, and more.)
Both students and faculty have names, postal addresses, and email addresses; each student also has a student number, a list of courses taken, and a list of courses they are currently enrolled in. Each faculty member has a faculty number and a list of courses they are currently teaching. (Again, this is a simplification.)
You’ll have a Faculty class and a Student class. You need both of them to have names, addresses, and email addresses, but duplicate code is generally a bad thing; so, avoid it by also defining a class, perhaps called Member, and keeping track of those features in Member. Then make both Faculty and Student subclasses of Member:
| | class Member: |
| | """ A member of a university. """ |
| | |
| | def __init__(self, name: str, address: str, email: str) -> None: |
| | """Initialize a new member named name, with home address and email |
| | address. |
| | """ |
| | |
| | self.name = name |
| | self.address = address |
| | self.email = email |
| | |
| | class Faculty(Member): |
| | """ A faculty member at a university. """ |
| | |
| | def __init__(self, name: str, address: str, email: str, |
| | faculty_num: str) -> None: |
| | """Initialize a new faculty named name, with home address, email |
| | address, faculty number faculty_num, and empty list of |
| | courses. |
| | """ |
| | |
| | super().__init__(name, address, email) |
| | self.faculty_number = faculty_num |
| | self.courses_teaching = [] |
| | |
| | |
| | class Student(Member): |
| | """ A student member at a university. """ |
| | |
| | def __init__(self, name: str, address: str, email: str, |
| | student_num: str) -> None: |
| | """Initialize a new student named name, with home address, email |
| | address, student number student_num, an empty list of courses |
| | taken, and an empty list of current courses. |
| | """ |
| | |
| | super().__init__(name, address, email) |
| | self.student_number = student_num |
| | self.courses_taken = [] |
| | self.courses_taking = [] |
Both class headers—class Faculty(Member): and class Student(Member):—tell Python that Faculty and Student are subclasses of the class Member. That means they inherit all the attributes of the class Member.
The first lines of both Faculty.__init__ and Student.__init__ call the function super, which produces a reference to the superclass part of the object, Member. That means that both of those first lines call the method __init__, which was inherited from the class Member. Notice that you pass the relevant parameters in as arguments to this call, just as you would with any method call.
If you import these into the shell, you can create both faculty and students:
| | >>> paul = Faculty('Paul Gries', 'Ajax', '[email protected]', '1234') |
| | >>> paul.name |
| | Paul Gries |
| | >>> paul.email |
| | [email protected] |
| | >>> paul.faculty_number |
| | 1234 |
| | >>> jen = Student('Jen Campbell', 'Toronto', '[email protected]', |
| | ... '4321') |
| | >>> jen.name |
| | Jen Campbell |
| | >>> jen.email |
| | [email protected] |
| | >>> jen.student_number |
| | 4321 |
Both the Faculty and Student objects have inherited the features defined in the class Member.
Often, you’ll want to extend the behavior inherited from a superclass. As an example, you might write a __str__ method inside the class Member:
| | def __str__(self) -> str: |
| | """Return a string representation of this Member. |
| | |
| | >>> member = Member('Paul', 'Ajax', '[email protected]') |
| | >>> member.__str__() |
| | 'Paul\\nAjax\\[email protected]' |
| | """ |
| | |
| | return f'{self.name}\n{self.address}\n{self.email}' |
With this method added to class Member, both Faculty and Student inherit it:
| | >>> paul = Faculty('Paul', 'Ajax', '[email protected]', '1234') |
| | >>> str(paul) |
| | 'Paul\nAjax\[email protected]' |
| | >>> print(paul) |
| | Paul |
| | Ajax |
| | [email protected] |
That isn’t quite enough, though: for class Faculty, you want to extend what the Member’s __str__ does, adding the faculty number and the list of courses the faculty member is teaching, and a Student string should include the equivalent student-specific information.
Use super again to access the inherited Member.__str__ method and to append the Faculty-specific information:
| | def __str__(self) -> str: |
| | """Return a string representation of this Faculty. |
| | |
| | >>> faculty = Faculty('Paul', 'Ajax', '[email protected]', '1234') |
| | >>> faculty.__str__() |
| | 'Paul\\nAjax\\[email protected]\\n1234\\nCourses: ' |
| | """ |
| | |
| | member_string = super().__str__() |
| | |
| | return f'''{member_string} |
| | {self.faculty_number} |
| | Courses: {' '.join(self.courses_teaching)}''' |
With this, you get the desired output:
| | >>> paul = Faculty('Paul', 'Ajax', '[email protected]', '1234') |
| | >>> str(paul) |
| | 'Paul\nAjax\[email protected]\n1234\nCourses: ' |
| | >>> print(paul) |
| | Paul |
| | Ajax |
| | [email protected] |
| | 1234 |
| | Courses: |