Object-Oriented Programming
Classes
You can define your own classes (i.e., object types) in Python, which can then be used alongside the built-in classes such as list
, int
, str
, etc.
The syntax for defining a class:
class ClassName:
# <statement-1>
.
.
.
# <statement-N>
It is customary to use Upper Camel Case for class names.
The syntax for creating (also called instantiating) an object of the class is ClassName()
.
The code below defines a class named Employee
, creates two employee objects, and prints the class/type of each object.
class Employee:
pass # an empty class
john = Employee() # create an Employee object
print(type(john)) # print type of the john object
alice = Employee()
print(type(alice))
Note how the type of each object is given as <class 'Employee'>
i.e., a class with name Employee
.
<class 'Employee'>
<class 'Employee'>
You can add methods to the class by defining them inside the class definition. Note that a method of a class always take self
as the first parameter. self
refers to the object itself. When calling the method, there is no need to supply an argument for the self
parameter as the target object is implicitly taken as the argument for that parameter.
Consider the code below:
| → |
|
write(self, text)
method is called as p.write('It was a dark night ...')
.
This is how the arguments are matched with the parameters:
self
→p
text
→'It was a dark night ...'
You can specify how to initialize an object of a class by defining an __init__()
method in the class. Here are the important things to note about the __init__()
method:
- There are two underscores in front and two behind the word
init
.
_init_(self)
__init__(self)
- It will be called every time you create an instance of the class.
- If it has parameters, you need to provide arguments for those parameters when you instantiate an object of that class.
This example shows an __init__
method added to a Person
class.
| → |
|
An object can have attributes i.e., values attached to the object, just as an object can have methods. Attributes and methods of an object can be accessed using the objectname.
syntax, as you did with objects of built-in classes.
This example shows accessing the get_age()
method and the birthday
attribute of an object x
.
| → |
|
The code within a class needs to use the self.
to refer to its own attributes and methods. Furthermore, the best place to initialize attributes is the __init__()
method.
Note how the __init__()
method of the Book
class initializes its two attributes and calls another of its own methods i.e., self.describe()
.
class Book:
def __init__(self, book_title, book_author):
self.title = book_title # initialize attribute title
self.author = book_author # initialize attribute author
self.describe() # call another method of the class
def describe(self):
# print attributes of the object
print('Book info:', self.title, '/by', self.author)
book1 = Book('The Jungle Book', 'Leo Tolstoy')
book2 = Book('The Art of War', 'Sun Tzu')
print() # print a blank line
print('Title:', book1.title)
print('Author:', book1.author)
book1.describe()
Book info: The Jungle Book /by Leo Tolstoy
Book info: The Art of War /by Sun Tzu
Title: The Jungle Book
Author: Leo Tolstoy
Book info: The Jungle Book /by Leo Tolstoy
You can get your classes to work with each other.
In this example we define a ReadingList
class that can store a list of Book
objects.
class ReadingList:
def __init__(self, initial_list):
self.books = initial_list
def add_book(self, book):
self.books.append(book)
def show_authors(self):
for b in self.books:
print(b.author)
my_list = ReadingList([book1]) # book1 defined in a previous example
my_list.add_book(book2) # book2 defined in a previous example
my_list.show_authors()
Leo Tolstoy
Sun Tzu
Class-Level Members
Attributes initialized inside a class but outside of its methods are considered class-level attributes (attributes are also called data or variables) i.e., they are shared among all objects of that class whereas attributes initialized inside the __init__()
method are instance-level attributes i.e., their values vary from instance-to-instance. A class-level attribute is accessed using the ClassName.
syntax.
The Student
class below has one class-level attribute total_students
. As it is meant to track the total number of Student
objects created, it is incremented in the __init__()
method. course_count
and name
are instance-level attributes.
class Student:
total_students = 0 # class-level attribute
def __init__(self, student_name):
self.name = student_name # instance-level attribute
Student.total_students = Student.total_students + 1 # update class-level attribute
self.course_count = 0 # instance-level attribute
The code below creates two Student
objects. Note how the class-level attribute total_students
,
- can be accessed using the class name (i.e.,
Student.total_students
) or the object (i.e.,amy.total_students
) and both give the same value. - is shared among all objects and correctly gives
2
after anotherStudent
object has been created.
amy = Student("Amy")
print('total students:', Student.total_students) # access via class name
print('total students:', amy.total_students) # access via object
print('Amy course count:', amy.course_count)
ben = Student("Ben")
print('total students:', Student.total_students)
print('total students:', ben.total_students)
print('Ben course count:', ben.course_count)
total students: 1
total students: 1
Amy course count: 0
total students: 2
total students: 2
Ben course count: 0
It is possible to have class-level methods too. A class-level method definition should be annotated using a @classmethod
annotation. In addition, a class-level method's first parameter should be cls
which represents the class itself (similar to instance level methods' first parameter needing to be self
).
The Printer
class below has a class-level attribute enabled
and class-level methods enable()
and disable()
.
class Printer:
enabled = True
def __init__(self, new_text):
self.text = new_text
def print_text(self):
if Printer.enabled:
print(self.text)
@classmethod
def disable(cls):
cls.enabled = False # cls here is same as Printer
print('All Printers disabled')
@classmethod
def enable(cls):
cls.enabled = True
print('All Printers enabled')
The code below uses the class-level method to enable/disable printing for all Printer
objects.
| → |
|
As you can see from the above code, class-level methods can be called even before any objects of that class have been created.
Visibility
The encapsulation aspect of OOP requires that an object should only allow controlled access to its members. For example, it should be able to make some of its attributes visible to other object while keeping others hidden. As a consequence of Python's aim to be a very flexible and versatile language, it does not enforce encapsulation as strictly as some other OOP languages. However, it recommends some conventions, if followed, will maintain a reasonable level of encapsulation in objects.
Private members: An attribute/method whose name has at least two leading underscores and at most one trailing underscore (e.g.,
__foo
,__bar_()
) cannot be accessed using its name by code outside the class.Protected members: a name prefixed with a single underscore (e.g.
_foo
,_bar()
) are -- by convention -- should not be accessed by code outside of the owner class as they are not meant to be part of the object's public interface.Public members: other members are considered public and OK to be accessed by code outside the class.
Consider the Account
class below.
class Account:
currency = '$' # public
_min = 10. # protected
__max = 1000.0 # private
def __init__(self, person, amount):
self.__balance = amount # private
self._owner = person # protected
self.status = 'OK' # public
def get_info(self): # public
return [self.status]
def get_loan_limit(self): # public
return self.__get_income()*5
def _get_more_info(self): # protected
return self.get_info() + [self._owner]
def __get_income(self): # private
return self.__balance + 100
The following code works fine because the members being accessed are public.
| → |
|
The following code works too. But they access protected members which is not a recommended practice.
| → |
|
The following code will not work because they try to access private members.
print('maximum allowed:', Account.__max) # error
a = Account('Adam', 100)
print('balance:', a.__balance) # error
print('income:', a.__get_income()) # error
AttributeError: type object 'Account' has no attribute '__max'...
AttributeError: 'Account' object has no attribute '__balance'...
AttributeError: 'Account' object has no attribute '__get_income'...
Although this example does not have class-level methods, note that the same visibility conventions apply to them as well.
Inheritance
You can make a class from another class. If you do, it is as if the already has all the attributes and methods of the .
Syntax:
class ChildClassName(ParentClassName):
# statements of the class
Consider the Person
class below:
| → |
|
The Teacher
class below inherits from the Person
class given above.
| → |
|
Observe how,
- a
Teacher
object can use theprint_info()
method defined in the parent class. - the statement
dan = Teacher('Dan')
invokes the__init__()
method defined in the parent class too. - the statement
print('the name is', dan.name)
is accessing the attributename
from aTeacher
object although the attribute is defined in the parent class. - the method
teach
accesses the attributename
usingself.name
although the attribute is defined in the parent class.
A child class can override a method defined in the parent class. That way, a child object can change a behavior defined in the parent class.
Note how the Student
class below overrides the print_info()
method of the parent class Person
.
| → |
|
When overriding methods, you can reuse the parent's definition of the same method using the super().
prefix.
class Person:
def __init__(self, name):
self.name = name
Given that Person
class has the initializer method given above, the following two versions of the Student
class are equivalent.
(a) Override without reusing parent's method
class Student(Person):
def __init__(self, name, matric):
self.name = name
self.matric = matric
(b) Override but reuse parent's method (preferred)
class Student(Person):
def __init__(self, name, matric):
super().__init__(name) # reuse parent's method
self.matric = matric
Note that all python classes automatically inherits from the built-in class object
even if you did not specify it as the parent class. The object
class has a __str__()
method that you can ovrride in your classes to customize how the print
function will print an object of your class.
The Book
class below overrides the __str__()
method so that Book
objects can be printed in a specific format.
| → |
|
A class can inherit from multiple classes. If multiple parent classes have the same method, the one that is given first (in the order of inheritance) will be used.
|
The TeachingAssistant
class above inherits from both Student
class and the Teacher
class both of which inherit from the Person
class. That means a TeachingAssistant
object can use methods from classes object
, Person
, Student
, Teacher
, and TeachingAssistant
.
| → |
|
As both Teacher
and Student
classes have the print_info()
method, the method from the Teacher
class will be used as it comes first in the inheritance order (Teacher, Student)
; that is why you see Elsie is a teacher
in the output instead of Elsie is a student
.
You need to use inheritance when you create user-defined exceptions because all such exceptions need to inherit from a built-in Exception
class.
In the example below, EmptyCommandError
and InvalidCommandError
are user-defined exceptions. The latter has overridden the constructor to take additional parameters.
class EmptyCommandError(Exception):
"""Indicates a task has expired."""
pass
class InvalidCommandError(Exception):
"""Indicates that the user entered an invalid command"""
def __init__(self, command, explanation):
self.command = command
self.explanation = explanation
def execute_command(command):
if command == '':
raise EmptyCommandError()
elif len(command) < 4:
raise InvalidCommandError(command, "command too short")
def process(command):
try:
execute_command(command)
except EmptyCommandError:
print('empty command')
except InvalidCommandError as e:
print('invalid command:', e.command, '->', e.explanation)
process('')
process('HA')
empty command
invalid command: HA -> command too short