type(10), type(int), type(type)
>>> (<class 'int'>, <class 'type'>, <class 'type'>)
Object oriented programming, commonly referred to as OOP, is fundamental to how objects are defined and used in a programming language.
There are 2 components of OOP
For example int
is a class (type) and there can be multiple instances like 1
, 2
etc. Note that int
is a class and its definition (code) to create int
type objects is stored once on RAM. All instances, on creation are stored on RAM separately.
In Python, type
is the root class of all classes, that is the reason type
and class are used interchangeably. type
can also be used as a function to check the class of an object.
type(10), type(int), type(type)
>>> (<class 'int'>, <class 'type'>, <class 'type'>)
Class is the blueprint of a certain type of object, what attributes all instance objects of this type will share
Functions defined in a class are called methods. Methods are regular functions with some differences to provide features related to classes and instances.
Class instance object is an instance object of certain class (type
) created and stored on RAM
At a more abstract level, OOP provide
OOP is generally used by developers for building tools and packages. Almost everything that can be implemented using OOP, can be implemented using functional programming. Functional programming keeps the complexity low. As a user it is recommended to know basics of OOP to use solutions provided by developers efficiently and actually use OOP only if necessary after getting some experience.
Class and instances have their own namespaces. Instance object namespace is searched first then class namespace is searched by the interpreter for variables.
Class data attributes are shared across all instance objects. They are suited for data attributes that do not define the state of an object. Main advantage is that they can be configured centrally. If the value is changed all instance objects have access to the new value.
Instance data attributes define the state of an instance object. If the value is changed in one of the instances, it does not impact the value of the attribute in other instance objects.
Bare data attributes are attributes that can be accessed and modified directly without any pre or post functionality.
Properties hide bare data attributes behind a property name, which if referenced from any instance object, act upon the bare attribute. Properties are also used to add some pre and post code while accessing or modifying the bare data attribute.
Static properties do not calculate the values of the underlying bare data attribute.
Dynamic properties do calculate the values of the underlying bare data attribute based on some other values.
Methods are callable attributes. Methods are simply functions defined in a class and they behave with minor difference compared to regular functions, to provide some basic functionality related to classes and objects.
By default methods defined inside a class are instance methods. They can be declared to be class or static using decorator syntax.
Instance methods are callable attributes intended to be called from instance objects. They are functions with access to the instance object itself. The first parameter is enforced to be the instance object. They cannot be called from the class directly.
Class methods are callable attributes that are accessible from both class and all instance objects, with access to class attributes. The first parameter is enforced to be the class object.
Static methods are callable attributes that are accessible from both class and all instance objects. They are regular functions without any enforced parameter.
class BasicClass:
= "bare class attribute" data_attr
.
) BasicClass.data_attr
>>> 'bare class attribute'
= BasicClass()
obj_1 = BasicClass() obj_2
.
) obj_1.data_attr
>>> 'bare class attribute'
obj_2.data_attr
>>> 'bare class attribute'
type(obj_1), type(obj_2)
>>> (<class '__main__.BasicClass'>, <class '__main__.BasicClass'>)
= "new value"
BasicClass.data_attr obj_1.data_attr, obj_2.data_attr
>>> ('new value', 'new value')
class CustomClass:
def custom_method():
print("running custom_method")
CustomClass.custom_method()
>>> running custom_method
= CustomClass()
obj_1 obj_1.custom_method()
>>> Error: TypeError: CustomClass.custom_method() takes 0 positional arguments but 1 was given
self
argumentself
self
for consistencyclass CustomClass:
def custom_method(self):
print("running custom_method")
print(self)
CustomClass.custom_method()
>>> Error: TypeError: CustomClass.custom_method() missing 1 required positional argument: 'self'
class CustomClass:
def custom_method(self):
print("running custom_method")
print(self)
= CustomClass()
obj_1 obj_1.custom_method()
>>> running custom_method
>>> <__main__.CustomClass object at 0x7035f8f4e650>
__init__
__init__
is an instance method called at the time of instance object creation
class CustomClass:
def __init__(self, bare_data_attr_1_val, bare_data_attr_2_val):
self.bare_data_attr_1 = bare_data_attr_1_val
self.bare_data_attr_2 = bare_data_attr_2_val
def custom_method(self):
print('running custom_method')
print(f'with access to {self.bare_data_attr_1 = }')
print(f'and {self.bare_data_attr_2 = }')
= CustomClass(2, 4)
obj_1 obj_1.bare_data_attr_1, obj_1.bare_data_attr_2
>>> (2, 4)
obj_1.custom_method()
>>> running custom_method
>>> with access to self.bare_data_attr_1 = 2
>>> and self.bare_data_attr_2 = 4
getattr(obj_1, "bare_data_attr_1")
>>> 2
setattr(obj_1, "bare_data_attr_1", "abc")
getattr(obj_1, "bare_data_attr_1")
>>> 'abc'
In other languages there is a concept of making certain attributes private
To define a property manually use
property(fget, fset, fdel, doc)
fget
, fset
and fdel
are instance methods to get, set and delete property valuedel
reserved word is used to delete attributes
It is convention to name the underlying attribute name to be _<property_name>
Using methods to access, modify and delete attributes helps with
Below example illustrates all aspects of defining a property in a class. Note the difference in names to illustrate different components of the property.
It is recommended to experiment in jupyter notebook by creating instance objects and accessing/modifying/deleting property and bare instance attribute.
class CustomClass:
"""This is CustomClass with a bare data attribute and a property"""
def __init__(self, property_1_val, bare_data_attr_1_val):
self.property_1_name = property_1_val
self.bare_data_atrr_1_name = bare_data_attr_1_val
def get_property_1_name(self):
print("getter called..")
return self._property_1_name
def set_property_1_name(self, property_1_val):
print("setter called..")
# required checks or calculations
self._property_1_name = property_1_val
def del_property_1_name(self):
print("delete method called..")
# required checks or calculations
del self._property_1_name
= property(fget=get_property_1_name,
property_1_name =set_property_1_name, fdel=del_property_1_name,
fset="""The property's description.""") doc
Below is the same example with Python decorator syntax. It is useful for cleaner syntax which is much easier to read.
class CustomClass:
"""This is CustomClass with a bare data attribute and a property"""
def __init__(self, property_1_val, bare_data_attr_1_val):
self.property_1_name = property_1_val
self.bare_data_atrr_1_name = bare_data_attr_1_val
@property
def property_1_name(self):
"""Property description"""
print("getter called..")
return self._property_1_name
@property_1_name.setter
def property_1_name(self, property_1_val):
print("setter called..")
# required checks or calculations
self._property_1_name = property_1_val
@property_1_name.deleter
def property_1_name(self):
print("delete method called..")
# required checks or calculations
del self._property_1_name
Class methods can be defined using @classmethod
decorator
Like instance methods, first parameter is mandatory and gives access to class
cls
class CustomClass:
= 10
x @classmethod
def some_class_method(cls):
print(f"This is a class method bound to class - {cls}")
print(f"has access to class attributes {cls.x = }")
CustomClass.some_class_method()
>>> This is a class method bound to class - <class '__main__.CustomClass'>
>>> has access to class attributes cls.x = 10
= CustomClass()
obj obj.some_class_method()
>>> This is a class method bound to class - <class '__main__.CustomClass'>
>>> has access to class attributes cls.x = 10
Static methods are regular functions defined in a class
Defined using @staticmethod
decorator
Can be accessed from class and all instances
There is no mandatory first parameter
class CustomClass:
@staticmethod
def some_static_method(a=10):
print("This is a static function")
print(f"a regular function with parameters, e.g. {a = }")
CustomClass.some_static_method()
>>> This is a static function
>>> a regular function with parameters, e.g. a = 10
= CustomClass()
obj = 100) obj.some_static_method(a
>>> This is a static function
>>> a regular function with parameters, e.g. a = 100
Below is a full example to illustrate all the pieces together. There is a Circle
class to create circle objects with the following components
pi
is a class data attribute_radius
is instance data attribute marked private
radius
is a static property to access _radius
from instancesarea
is a dynamic property with no set method
_area
circumference
is an instance methodclass Circle:
def __init__(self, radius_val):
self.radius = radius_val
= 3.141592653589793
pi
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, radius_val):
print("setting radius and calculating area")
self._radius = radius_val
self._area = self.pi * (self.radius**2)
@property
def area(self):
return self._area
def calc_circumference(self):
print('calculating circumference')
return 2*self.pi*self.radius
= Circle(1) c1
>>> setting radius and calculating area
c1.radius, c1.area, c1.calc_circumference()
>>> calculating circumference
>>> (1, 3.141592653589793, 6.283185307179586)
c1.area, c1.calc_circumference()
>>> calculating circumference
>>> (3.141592653589793, 6.283185307179586)
= 2 c1.radius
>>> setting radius and calculating area
class
defines classes which are callable and used to create instance objects
__init__
method is used to control creation of attributes at instance object creation
Access attributes
<obj_name>.<attr_name>[()]
getattr(<obj_name>, <attr_name>)
Modify attributes
<obj_name>.<attr_name> = <something>
setattr(<obj_name>, <attr_name>, <something>)
Delete attributes
del <obj_name>.<attr_name>
delattr(<obj_name>, <attr_name>)
Data attributes
__init__
property()
or @property
, static or calculatedMethods (operations)
self
)
@classmethod
decoratorcls
)@staticmethod
decoratorBelow are some topics needed for advanced usage of programming. Understanding these topics leads to understanding of how much of the functionality provided in Python is implemented.
__init__
, __str__
, __repr__
, __add__
, …__
in variable names to avoid clashesInheritance means classes can inherit attributes from other classes, which allows classes to be structured and joined in efficient ways.
Single inheritance refers to case where a class can inherit attributes from a single class. A hierarchy is built when a class inherits from another class. The class that inherits is called the sub class. The class from which the sub class inherits is called the base class.
Multiple inheritance refers to case when a class can inherit attributes from multiple classes. Attributes are inherited following a recursive algorithm. More details are documented at Python documentation.
There are some in-built functions provided to check the class hierarchy.
isinstance(<obj>, <type>)
issubclass(<sub class>, <base class>)
For example bool
is derived from int
therefore isinstance(True, int)
and issubclass(bool, int)
both return True
.
isinstance(True, int), issubclass(bool, int)
>>> (True, True)