Guidance for the item(s) below:
You are strongly recommended to do the following when learning this week's topics:
This week, we continue to learn more about Python classes. Let's start with class-level variables and methods.
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
,
Student.total_students
) or the object (i.e., amy.total_students
) and both give the same value.2
after another Student
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.
Guidance for the item(s) below:
Next, let us learn to to control access to variables and methods of an object.
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.
Follow up notes for the item(s) above:
This video contains an example that covers some of the OOP topics covered so far.
Guidance for the item(s) below:
As your project code gets bigger, it becomes cumbersome to have all code in one file. Let's learn how to organize Python code into files and folders.
You can organize the code as multiple files. Python considers each file as a module. To use a function/class in one file from another, you need to import
it first.
Consider the following two files residing in the same directory.
[project_root]/student.py
:
def describe(name):
print(name, 'is a student')
[project_root]/course.py
:
import student
student.describe('Adam')
Notice how the course.py
imports the student
module and uses the student.describe
function defined in the student.py
file.
You can organize files into sub-directories too.
Consider the following two files.
[project_root]/utils/print_utils/misc.py
:
def print_as_list(text):
print(list(text))
[project_root]/course.py
:
from utils.print_utils import misc
misc.print_as_list('Adam')
Notice how the import
statement uses a slightly different syntax and uses .
to indicate directory nesting (i.e., utils/print_utils
→ utils.print_utils
). In fact, there are other variations of the import
syntax.
When you import a module, Python will interpret and execute any code in it.
Consider this student
module being imported into the course
module.
[project_root]/student.py
:
def describe(name):
print(name, 'is a student')
describe('Betty')
| → |
|
Notice how the output contains Betty is a student
. That is because the line describe('Betty')
in student.py
is being executed as the student
module is being imported.
To prevent execution of statements in an imported module, nest such statements under a if __name__ == "__main__":
block.
In the code below, the student
module can be executed to get the following output.
| → |
|
When the student
module is imported to another module, describe('Betty')
line is no longer executed.
| → |
|