- Class: A blueprint of creating objects of a particular type
- Methods: Regular functions that are part of a class
- Attributes: Variables that hold data that are part of a class
- Object: a specific instance of a class
- Inheritance: means by which a class can inherit capabilities from another
- Composition: Means of building complex objects out of other objects
class
keyword is used to create a new class. Parentheses after class name is required only when you are inheriting
another class. The built-in initialization function is __init__
. It can be overridden. This function is the
first one to be called when the object is created. To repeat, this function is implicitly called after object is created.
Therefore, it will not be appropriate to call it the constructor. Rather, it is the initializing function, for initializing
attributes.
All methods(procedural attributes) have their first attribute as the object itself. This parameter is by convention called
self
. Of course, there are exceptions to the first parameter being the object itself.
class Book:
def __init__(self, title, price):
self.title = title
self.price = price
self.__secret = "Hello there!"
def get_price(self):
if hasattr(self, '_discount'):
return self.price - (self.price * self._discount)
else:
return self.price
def set_discount(self, amount):
self._discount = amount
b = Book("War and Peace", 39.95)
print(b.title) # War and Peace
print(b.get_price()) # 39.95
b.set_discount(0.25)
print(b.get_price()) # 29.9625
print(b._Book__secret) # Hello there!
print(type(b)) # <class '__main__.Book'>
print(isinstance(b, Book)) # True
print(isinstance(b, object)) # True
Here title
is called the instance attribute
, because the value to this attribute is unique for each instance. Similarly,
instance methods
will have their first parameter as the object as self
. In the set_discount
method, a new instance
variable is created _discount
. The underscore(_) in the variable name indicates that this attribute should not be called
outside the class. This attribute is for internal purpose only. In the function get_price
, the function hasattr
is used
to check if the object has the instance attribute _discount
. The instance atrribute __secret
has a double underscore.
Compiler prevents usage of this attribute outside the class by internally changing the attribute name. That is called name
mangling. Compiler adds the class name in front of the such attributes.
There are two ways of check the type of an object. One way is using the built-in type
function and checking the output string,
or using isinstance
function. This function checks for parent class as well. That is why, isinstance(b, object)
return true,
because all classes in Python inherit object
class. Meaning, object
is the most basic data type.
class Book:
book_types = ("hard cover", "paper back", "ebook")
@classmethod
def get_book_types(cls):
return cls.book_types
def __init__(self, title, booktype):
self.title = title
if not booktype in Book.book_types:
raise ValueError(f"{booktype} is not a valid book type")
else:
self.book_type = booktype
print(Book.get_book_types()) # ('hard cover', 'paper back', 'ebook')
b1 = Book("Title1", "hard cover")
b2 = Book("Title2", "comic") # ValueError: comic is not a valid book type
book_type
is a class attribute. It is not unique to an instance. Any change made to this attribute will be adopted by all objects,
even by the one already created. Similarly, get_book_types
is a class method. They work on class. That is why, there is atleast one
parameter to such method, that is the class itself.
class Book:
__bookList = None
@staticmethod
def getbooklist():
if Book.__bookList is None:
Book.__bookList = []
return Book.__bookList
def __init__(self, title, booktype):
self.title = title
self.book_type = booktype
b1 = Book("Title 1", "hard")
b2 = Book("Title 2", "soft")
# use the static method to access a singleton object
books = Book.getbooklist()
books.append(b1)
books.append(b2)
print(books) # [<__main__.Book object at 0x01087178>, <__main__.Book object at 0x010871D8>]
Static methods are global functions in the class namespace. It is used when a singleton object is created for the class, like in this
case, the attribute __bookList
. There are not many great usages of static methods.
There are other usages of annotations, like @property
can used for attribute getter functions, and @<instance_name>.setter
can be used as attribute setter function.
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, width):
if width <= 0:
raise ValueError("width has to be positive")
self._width = width
r = Rectangle(10, 20)
print(r.width) # 10
# r.width = -10 # ValueError: width has to be positive
r1 = Rectangle(-10, 20) # works unless __init__ also uses setter getter functions
It defines a way to inherit attributes from one or many base classes. This can be used to reduce duplications. For example,
a base class named Publication
with title and price attributes, can be inherited by classes Magazine, Newspaper, and Book.
class Publication:
def __init__(self, title, price):
self.tittle = title
self.price = price
class Periodical(Publication):
def __init__(self, title, price, publisher, period):
super().__init__(title, price)
self.publisher = publisher
self.period = period
class Book(Publication):
def __init__(self, title, price, pages, author):
super().__init__(title, price)
self.pages = pages
self.author = author
class Newspaper(Periodical):
def __init__(self, title, price, publisher, period):
super().__init__(title, price, publisher, period)
class Magazine(Periodical):
def __init__(self, title, price, publisher, period):
super().__init__(title, price, publisher, period)
Abstract Class
are templates for the inheriting class to follow. They have methods that child class has to implement.
Basically, they are used to enforce class constraints. One more constraint is that user will not be allowed to create an
object of the abstract class. All this constraints are followed by using abc
standard module.
from abc import ABC, abstractmethod
class GraphicShape(ABC):
def __init__(self):
super().__init__()
@abstractmethod
def calcArea(self):
pass
class Circle(GraphicShape):
def __init__(self, radius):
super().__init__()
self.radius = radius
def calcArea(self):
return 3.14 * (self.radius ** 2)
class Square(GraphicShape):
def __init__(self, side):
super().__init__()
self.side = side
def calcArea(self):
return self.side ** 2
c = Circle(10)
print(c.calcArea()) # 314
s = Square(10)
print(s.calcArea()) # 100
g = GraphicShape() # TypeError: Can't instantiate abstract class GraphicShape with abstract methods calcArea
Python allows multiple inheritance, by mentioning the class names in the parentheses, separated by comma. This type of inheritance should be used carefully.
class A:
def __init__(self):
super().__init__()
self.foo = "foo"
self.name = "class A"
class B:
def __init__(self):
super().__init__()
self.bar = "bar"
self.name = "class B"
class C(A, B):
def __init__(self):
super().__init__()
def showprops(self):
print(self.foo) # foo
print(self.bar) # bar
print(self.name) # class A
c = C()
c.showprops()
print(C.__mro__) # (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
All classes, be it base or child, should implement super().__init()
. Method Resolution Order (MRO) is a way in which complier
searches for attributes. It always searches the current class first. Then the parent classes in the order they are mentioned
inside parentheses. You can see the MRO using the function __mro__
called on class.
Multiple Inheritance is important while creating interfaces.
Python does not provide interface implementation. However, it can be written using abstract as well as multiple inheritance concept.
from abc import ABC, abstractmethod
class JSONify(ABC):
@abstractmethod
def toJSON(self):
pass
class GraphicShape(ABC):
def __init__(self):
super().__init__()
@abstractmethod
def calcArea(self):
pass
class Circle(GraphicShape, JSONify):
def __init__(self, radius):
super().__init__()
self.radius = radius
def calcArea(self):
return 3.14 * (self.radius ** 2)
def toJSON(self):
return f"{{\"circle\":{str(self.calcArea())} }}"
c = Circle(10)
print(c.calcArea()) # 314
print(c.toJSON()) # {"circle":314.0 }
Interfaces are classes where all methods are by default abstract.
It builds objects out of other objects. It has more of a has relationship
. Book has an attribute author. Author has
attributes firstname and lastname. Inheritance and composition can be combined. It is used to reduce a monolithic structure
into a simplified format.
class Author:
def __init__(self, fname, lname):
self.fname = fname
self.lname = lname
def __str__(self):
return f"{self.fname} {self.lname}"
class Chapter:
def __init__(self, name, page_count):
self.name = name
self.page_count = page_count
class Book:
def __init__(self, title, price, author=None):
self.title = title
self.price = price
self.author = author
self.chapters = []
def add_chapter(self, chapter):
self.chapters.append(chapter)
def get_book_page_counts(self):
result = 0
for ch in self.chapters:
result += ch.page_count
return result
author = Author("Leo", "Tolstoy")
b = Book("War and Peace", 39.0, author)
b.add_chapter(Chapter("c1", 129))
b.add_chapter(Chapter("c2", 190))
b.add_chapter(Chapter("c3", 89))
print(b.title, b.author, b.get_book_page_counts()) # War and Peace Leo Tolstoy 408