In the previous tutorial, we learned about the sequences and unordered collections in Python, including the operations, functions, and methods applicable to them. However, there can be more complex data structures in an application, which are known as user-defined classes and objects.
Any application uses data (values/objects/user-defined classes/objects) and code behavior (how the data is manipulated).
If an application is designed in a manner that focuses on:
- Functionality (code behavior) – it’s called procedural or functional programming
- Data (values/objects/user-defined objects) – it’s called object-oriented programming
Although Python is an object-oriented programming language, it does not force an application to be defined by this type of design pattern. This means an application can be designed in a variety of patterns, including using procedural and model-view-control (MVC). It’s also possible to mix many design patterns in one application.
But it’s worth exploring the object-oriented features of Python as it is useful for creating user-defined data structures and efficiently architecting certain applications.
Classes are similar to a blueprint of real objects or data structures. So, in any application, a class might represent 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 also have functions that bind or manipulate attributes associated with it. Such functions are called methods.
In Python, there’s no difference between attributes and methods. Methods are treated as callable attributes.
Python classes are also objects, which can be used as a type to create other objects. 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 certain class type, it’s called an instance or an instance object of that class.
The instance object has all of the 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 that are defined.
A class can be a subclass of another class, where the parent class is known as a super class. Much like an object inherits the attributes of its class, if its class is a subclass, it implicitly inherits the attributes of its super class. A subclass also implicitly inherits the attributes and methods of its super class.
The class statement defines the classes and is a compound statement with this 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, where that class is a subclass. The base classes are optional and should only be included when the defined class is a subclass of another class.
This is an ideal 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 bind to a class are called class attributes. These attributes can be defined within the class body and outside of the class definition. The class attributes are accessed by the class_name.attribute_name syntax.
Here’s a valid example of defining a class attribute within the class body:
x = 25
Now, here’s a valid example of defining a class attribute outside of the class definition:
C1.x = 25
The functions bound to a class are called methods. Even if the class attributes are defined in the class body, when using the method definitions, they must be referenced in the class_name.attribute_name syntax.
This is a valid example of a method accessing the class attributes:
x = 25
When an attribute is called and the identifier is used as a reference for that class attribute, or a method starts with two underscores, 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 the attributes and methods bound to the class private and reduces any chance of collision with other references in the code.
A class can also have special methods with two leading and two trailing underscores as part of an identifier used for their reference. For example, __init__, __get__, __set__ and __delete__ are 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 bind, rebind, or unbind to them only through the dunder methods. Similar to how they can bind to the default data values/objects in the __init__ method, they can:
- Return their value only through the __get__ method
- Have their value modified only through the __set__ method
- May be deleted only through the __delete__ method.
For initializing, getting, setting, or deleting these attributes, the __init__, __get__, __set__ and __delete__ methods should be explicitly defined in a class body with the appropriate statements.
Such attributes are called descriptors. If a descriptor does not have the __set__ method defined, its value cannot be changed. Such descriptors are called nonoverriding or nondata descriptors. If a descriptor has the __set__ method defined, its value can be changed in the code by calling the __set__ method for it.
The descriptors with value can be changed and are called overriding descriptors. These descriptors cannot bind, rebind, or unbind through regular assignment statements.
Here’s 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
When creating an instance (instance object) of a class, a reference can be assigned to the class object as a function.
This is a valid example of creating an instance:
X = C1() #C1 is a class
If the __init__ method is supplied to a class, there can be default initialization of the attributes in that method. When an instance is created, any required arguments must be passed to the instantiation to fulfill the initialization.
Here’s an 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
In this case, the __init__ works like a constructor function in other object-oriented languages. An instance can also inherit the __init__ method from the super class of its class. If there is no __init__ method defined for the class or its super class in the instantiation, the class must be called without arguments.
This means there should be zero instance-specific arguments. Also, the __init__ method cannot return any value other than None. An instance can have its own attributes that haven’t been defined in the class body or for its class.
It’s also possible to create an instance by assigning a function to a reference. In this 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’ve discussed, any class definition can have base classes that are arguments. Such a class is then called a subclass and the base class is called a super class or parent class.
A subclass implicitly inherits all of the attributes and methods of its base class. Similarly, an instance of subclass inherits all of the attributes and methods of its class, as well as of the base class of its class. This feature of inherited attributes and methods from a parent object is called inheritance in the object-oriented paradigm.
A class may have multiple base classes. As such, it inherits the attributes and methods from all of the base classes. This is called multiple inheritances. It’s also possible that the base class might have another base class. If so, the object inherits the attributes and methods from its base class and the base class of its base class. This is called multi-level inheritances.
In the next tutorial, we’ll cover how to design graphic interfaces in Python.