Classes and OOP
Classes and OOP
Defining classes
A class in Python is effectively a data type. All the data types built into Python are classes. You
define a class with the class statement.
# the below code will give an error, it's just for explanation
class MyClass:
body
By convention, class identifiers are in CapCase—that is, the first letter of each component word
is capitalized. After you define the class, you can create a new object of the class type (an
instance of the class) by calling the class name as a function:
instance = MyClass()
The following short example defines a class called Circle, creates a Circle instance, assigns a
value to the radius field of the circle, and then uses that field to calculate the circumference of
the circle:
The fields of an instance are accessed and assigned to by using dot notation.
The __init__ method is similar to a constructor in Java, but it doesn’t really construct anything;
it initializes fields of the class. Also unlike those in Java and C++, Python classes may only
have one __init__ method. This example creates circles with a radius of 1 by default:
1. By convention, self is always the name of the first argument of __init__ . self is set to the
newly created circle instance when __init__ is run
2. Next, the code uses the class definition. You first create a Circle instance object
3. The next line makes use of the fact that the radius field is already initialized.
4. You can also overwrite the radius field;
5. as a result, the last line prints a different result from the previous print statement
2. Instance variables
Instance variables are the most basic OOP feature. Looking at the Circle class defined above,
radius is an instance variable of Circle instances.
Each instance of the Circle class has its own copy of radius, the value stored in its copy might
be different from the values stored in the radius variable in other instances.
Methods are called with the dot notation (like with variables), except that we add parentheses at
the end and pass whatever arguments the function requires e.g. instance.method() ,
instance.method2(3, 4) .
We are going to add an area method to our Circle class:
>>> c = Circle()
>>> c.radius = 3
>>> print(c.area())
28.27431
>>> print(Circle.area(c))
28.27431
This syntax is known as unbound method invocation. The method is invoked with the class
name ( Circle ) dot the method name area and the first argument is the instance c . This
syntax is not commonly used.
Like functions, methods can be invoked with arguments if the method definition accepts them.
We are going to add an argument to the __init__ method so that we can initialize our circles
with a particular radius.
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return self.radius * self.radius * 3.14159
NB: self.radius is the instance variable while radius is a parameter to the __init__
function. In Python OOP, if a variable is not prefixed with the self keyword, that usually means
it refers to a local variable in the method and not an instance variable.
Using the above class, we can create circles of any radius with one call on the Circle class
e.g. c = Circle(5) creates a circle with radius 5.
All standard Python function features - default argument values, extra arguments ( *args ),
keyword arguments ( **kwargs ) and so forth - can be used with methods.
The first line of the __init__ could have been defined as:
def __init__(self, radius=1)
Then calls to circle would work with or without an extra argument; Circle() would return a
circle of radius 1, and Circle(3) would return a circle of radius 3.
3.1 Exercise:
Update the code for the Rectangle class so that you can set the dimensions when an instance
is created, just as for the Circle class above. Also, add an area() and perimeter() method.
4. Class variables
A class variable is a variable associated with a class, not an instance of a class, and is
accessible by all instances of the class.
A class variable might be used to keep track of some class-level information, such as how many
instances of the class have been created at any point.
A class variable is created by an assignment in the class body, not in the __init__ function.
After it has been created, it can be seen by all instances of the class.
We can use a class variable to make a value for pi accessible to all instances of the Circle
class:
class Circle:
pi = 3.14159
def __init__(self, radius):
self.radius = radius
def area(self):
return self.radius * self.radius * Circle.pi
>>> Circle.pi
3.14159
>>> Circle.pi = 4
>>> Circle.pi
4
>>> Circle.pi = 3.14159
>>> Circle.pi
3.14159
Class variables can also be accessed through instance methods in any of the following ways:
Using the class's name like in the area method defined above
Using the __class__ attribute of the instance i.e. the return statement in the area method
above can be replaced with: return self.radius * self.radius * self.__class__.pi .
This avoids hardcoding the class name.
Using the instance (self). The return statement can also be return self.radius *
self.radius * self.pi . This method has some drawbacks that I didn't think necessary to
include here.
Type the below code in a file if you haven't been typing it yet:
def area(self):
"""determine the area of the Circle"""
return self.__class__.pi * self.radius * self.radius
@staticmethod
def total_area():
"""Static method to total the areas of all Circles """
total = 0
for c in Circle.all_circles:
total = total + c.area()
return total
Notice the use of documentation strings (docstrings). This help to explain the use of modules,
classes or even functions. We can access the docstring of the circle module with the __doc__
special attribute e.g. circle.__doc__
>>> circle.__doc__
'circle module: contains the Circle class.'
>>> circle.Circle.__doc__
'Circle class'
>>> circle.Circle.area.__doc__
'determine the area of the Circle'
Class methods are (implicitly) passed the class they belong to as their first parameter, below
shows how the total_area from above will look as a class method
Create a new file, circle_cm.py and paste the code from the above circle module into it
Replace the total_area function definition in the circle_cm.py with this one:
By using a class method instead of a static method, you don't have to hardcode the class name
into total_area .
5.3 Exercise
Write a class method similar to total_area() that returns the total circumference of all circles
6 Inheritance
Let's take the case of a program that allows us to draw shapes (circles, rectangles, squares,
etc.) on particular positions on the screen.
To be able to do this, we need to store the position (x and y coordinates) of each instance of the
shape. We can do this by adding two new instance variables to our classes: x and y.
But this approach is tedious and redundant if we have to do it for many other shape classes.
So instead we create a super class called Shape that will initialize these position attributes and
each shape will inherit from this superclass.
class Shape:
def __init__(self, x, y)
self.x = x
self.y = y
class Circle(Shape):
def __init__(self, r=1, x=0, y=0):
super().__init__(x, y)
self.radius = r
The first requirement is specifying the parent classes, in parentheses, immediately after
the name of the class being defined.
The second and more subtle element is the necessity to explicitly call the __init__
method of inherited classes. Otherwise, in the example, instances of Circle and Square
wouldn’t have their x and y instance variables set.
Inheritance also comes into effect when you attempt to use a method defined in the parent
class (superclass).
To see this, define another method in the Shape class called move which moves a shape by a
given displacement. It modifies the x and y coordinates of the shape.
Add the following method definition to the Shape class:
After defining this method, we can use it in the child classes as follows:
>>> c = Circle(1)
>>> c.move(3, 4)
>>> c.x
3
>>> c.y
4
They enhance security and reliability by selectively denying access to parts of an object's
implementation
They prevent name clashes that can arise from the use of inheritance.
In python, any method or instance variable whose name begins - but doesn't end - with a
double underscore ( __ ) is private, everything else isn't private.
class Mine:
def __init__(self):
self.x = 2
self.__y = 3
def print_y(self):
print(self.__y)
>>> print(m.x)
2
>>> print(m.__y)
Traceback (innermost last):
File "<stdin>", line 1, in ?
AttributeError: 'Mine' object has no attribute '__y'
The print_y method isn’t private, and because it’s in the Mine class, it can access __y and print
it:
>>> m.print_y()
3
To create a property, you use the property decorator with a method that has the property's
name:
class Temperature:
def __init__(self):
self._temp_fahr = 0
@property
def temp(self):
return (self._temp_fahr - 32) * 5 / 9
Without a setter, the above property is read-only, trying to assign a value to it raises an
AttributeError:
>>> t = Temperature()
>>> t.temp
-17.77777777777778
>>> t.temp = 3
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[13], line 1
----> 1 t.temp = 3
AttributeError: property 'temp' of 'Temperature' object has no setter
In [14]:
To make the property writable, you need to add a setter to the class:
@temp.setter
def temp(self, new_temp):
self._temp_fahr = new_temp * 9 / 5 + 32
Now you can use standard dot notation to both get and set the property temp.
Notice that the name of the method remains the same, but the decorator changes to the
property name (temp, in this case), plus .setter indicates that a setter for the temp property is
being defined:
>>> t = Temperature()
>>> t._temp_fahr
0
>>> t.temp
-17.77777777777778
>>> t.temp = 34
>>> t._temp_fahr
93.2
>>> t.temp
34.0