In the previous tutorials, we discussed sequences and unordered collections in Python. We also learned about operations, functions, and methods applicable to them. There can also be more complex data structures in an application. The data values in these data structures can have a complex relationship and behavior. These data structures are defined as user-defined classes and objects.
Any application basically revolves around data (values/objects/user-defined classes/objects) and code behavior (how data is manipulated). If an application is designed in a manner that focuses on functionality (code behavior), it is called procedural or functional programming. If an application is designed in a manner that focuses on data (values/objects/user-defined objects), it is called object-oriented programming.
Python is an object-oriented programming language. However, it does not force us to design an application by object-oriented design pattern. In Python, an application can be designed in a variety of design patterns including procedural and model-view-control (MVC). It even possible to mix many design patterns in an application. It is important to explore the object-oriented features of Python as it will be useful in creating user-defined data structures and efficiently architecting our applications.
Classes are like a blueprint of real objects or data structures. So in any application, a class may be representing a real object or a data structure. The properties of the object/data structure are defined as attributes of the class. Attributes are references that may contain in-built or user-defined objects. A class may have some functions that bind to it that may manipulate attributes associated with it. Such functions are called methods. In Python, there is no difference between attributes and methods. Methods are treated as just being callable attributes.
Python classes are also objects. These are those objects that can be used as a type to create other objects. This is like data types in Python are also objects, but they are used to define the type of other objects. When an object is created of a class type, it is called an instance of that class or instance object of that class. The instance object has all attributes (non-callable and callable) that are defined for its class. Some attributes of an object inherited from its class can have default data values. Such non-callable and callable attributes are called descriptors. Any object can have additional attributes as well defined for itself.
A class can be a subclass of another class where the parent class is then known as super class. Like an object inherits attributes of its class, if its class is a subclass, it implicitly inherits attributes of its super class. A subclass also implicitly inherits attributes and methods of its super class.
The class statement defines the classes. The class statement is a compound statement having the following syntax:
class className (base-classes)
The className can be any identifier that serves as the reference to the class. The class definition can have base classes as comma-delimited arguments. The base classes are super classes of which this class is a subclass. The base classes are optional and should be included only when the defined class is a subclass of another class. This is the perfect example of using classes as objects. When base classes are passed as arguments, Python is treating them as objects. The statements that are included in the definition of a class are called the class body.
The attributes (non-callable or callable) that are bind to a class are called class attributes. These attributes can be defined within the class body as well as outside class definition. The class attributes are accessed by class_name.attribute_name syntax. The following is a valid example of defining class attribute within the class body:
x = 25
The following is a valid example of defining class attribute outside class definition:
C1.x = 25
The functions bound to a class are called methods. Even if class attributes are defined in the class body, in method definitions, they must be referenced in class_name.attribute_name syntax. The following is a valid example of a method accessing a class attributes:
x = 25
When an identifier is used as a reference for a class attribute or method starts with two underscores, whenever that attribute is called, the class name with a leading underscore is prefixed to the attribute name. So, if an identifier __getData is called, the Python compiler will implicitly change the identifier to _className__getData. Such identifiers are called private names or class private variables. This makes the references to attributes and methods bound to the class private and reduces any chance of collision with other references in the code.
A class can have special methods that have two leading underscores and two trailing underscores as part of an identifier used for their reference. For example, __init__, __get__, __set__ and __delete__ are some special methods that may be supplied by a class. These implicit methods have a special purpose in any class and are called dunder methods, magic methods, or special methods.
Some class attributes can have managed access. The data values can be bind, rebind, or unbind to them only through dunder methods. Like they can be bind to default data values/objects in __init__ method, they can return their value only through __get__ method, can have their value modified only through __set__ method and can be deleted only through __delete__ method. For initializing, getting, setting, or deleting these attributes, __init__, __get__, __set__ and __delete__ methods should be explicitly defined in a class body with appropriate statements. Such attributes are called descriptors. If a descriptor does not have __set__ method defined, its value cannot be changed. Such descriptors are called nonoverriding or nondata descriptors. If a descriptor has __set__ method defined, its value can be changed in the code by calling __set__ method for it. Such descriptors whose value can be changed are called overriding descriptors. The descriptors cannot be bind, rebind, or unbind through regular assignment statements. The following is a valid example of descriptors:
def __init__(self, value):
self.value = value
def __set__(self, *_):
def __get__(self, *_):
c = C1(25)
x = C2() #instance object of class C2|
print(x.c) # Prints 25
x.c = 52
print(x.c) # Still Prints 25
For creating an instance (instance object) of a class, a reference can be assigned class object as a function. The following is a valid example of creating instance:
X = C1() #C1 is a class
If __init__ method is supplied to a class, there can be some default initialization of attributes in that method. When an instance is created, any required arguments must be passed to the instantiation to fulfill initialization. Look at the following example:
def __init__(self, value1, value2):
self.a = value1
self.b = value2
y = C1(25, 34)
print(y.a) #Prints 25
print(y.b) #Prints 34
Here, __init__ works like a constructor function in other object-oriented languages. An instance can also inherit __init__ method from the super class of its class. If there is no __init__ method defined for the class and also not for its super class, in the instantiation, the class must be called without arguments. So, there should be no instance-specific arguments. It should be noted that __init__ method cannot return any value other than None. An instance can also have its own attributes that may not have been defined in the class body or defined for its class.
It is also possible to create an instance by assigning a function to a reference. In that case, the function must return a class object. Such functions are called factory functions.
Inheritance is a powerful feature in object-oriented programming. As we have already discussed that any class definition can have base classes as arguments. Such a class is then called subclass, and the base class is called a super class or parent class. A subclass implicitly inherits all attributes and methods of its base class. Similarly, an instance of subclass inherits all attributes and methods of its class as well as of the base class of its class. This feature of inheriting attributes and methods from a parent object is called inheritance in the object-oriented paradigm.
A class may have multiple base classes. In such a case, it inherits attributes and methods from all the base classes. This is called multiple inheritance. There can be a possibility that the base class may itself have another some base class. Then also, the object inherits attributes and methods from its base class as well as the base class of its base class. This is called multi-level inheritance.
In the next tutorial, we will discuss designing graphic interfaces in Python.