Python

Python Classes and Objects

In this article, we will explain classes and objects in Python.

Python supports code construction using the object-oriented paradigm. It is a procedural language that does not enforce that all code should be written using classes and objects. In that sense, it can be called an object-based language. In this article, we discuss the concepts of classes and objects in Python and some special features that Python provides for them.

You can also check this tutorial in the following video:

Python Classes and Objects – video

1. What is OOP?

Robert Martin in Clean Architecture notes that the object-oriented programming paradigm was started in 1966 by Ole Johan Dahl and Kristen Nygaard. He wrote that “these two programmers noticed that the function call stack frame in the ALGOL language could be moved to a heap, thereby allowing local variables declared by a function to exist long after the function returned. The function became a constructor for a class, the local variables became instance variables, and the nested functions became methods. This led inevitably to the discovery of polymorphism through the disciplined use of function pointers.”

As per Wikipedia, object-oriented programming (OOP) “is a programming paradigm based on the concept of ‘objects’, which can contain data and code: data in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods)”.

Uncle Bob extends the definition to lay its significance for the software architect. He states, “OO is the ability, through the use of polymorphism, to gain absolute control over every source code dependency in the system. It allows the architect to create a plugin architecture, in which modules that contain high-level policies are independent of modules that contain low-level details.” It is important for programmers to understand OOP as it leads to software architecture aspects like design patterns, domain-driven design and layered architecture. The three fundamental features of OOP are encapsulation, inheritance and polymorphism, which we are not covering in this article.

2. Key technicalities of Python objects and classes

2.1 Definition

A class is a programmer-defined type or a blueprint that has data and operations that can be performed. Objects are created using classes and each object is an instance of the class from which it is created.

2.2 Constructor

Functions defined inside classes are called methods. They represent an operation that can be performed on the object or services that it provides. Of these methods, there is a special method called constructor, which is automatically called when you create an object. The constructor defines and initializes the data contained in the object.

Python’s syntax for the constructor is:

def __init__(<parameters>):

To invoke the constructor, you have to call the class as a function, passing the arguments that the constructor expects.

2.3 Class and instance variables

Data of an object also called fields or attributes are simply variables stored in the object itself. There are two types of object variables: class variables and instance variables. Class variables are created only once for each class. Memory is allocated to them only at one location, regardless of how many objects of that class are instantiated. Class variables are defined outside the methods of a class and all objects share these variables. On the other hand, instance variables are created for every object. They are defined and initialized in the constructor and bound to the object with the self. notation.

2.4 Class and instance methods

Class methods are bound to the class whereas instance methods are bound to an object. The first parameter of an object method is ‘self‘. Object methods can modify the state of the object. To define a class method, it is decorated with the annotation @clasmethod , and instead of self, cls is used as the method parameter. Class methods are invoked as classname.methodname() or object.methodname(). They are typically used to write factory methods that return class objects for different use cases.

2.5 __repr__ and __str__

Python has a rich set of predefined methods that start and end with double underscores. Of these special methods, there are two that give a human-readable representation of an object: __repr__ and __str__. These methods can also be invoked via calls to repr() and str(). As per the Python help documentation, repr() is to “Return the canonical string representation of the object. For many object types, including most builtins, eval(repr(obj)) == obj. Create a new string object from the given object.” Whereas the functionality of str() is to “Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object).”

So, the stated intention is clear. The str() representation is for the end-users of your software application and is used mostly for logging purposes. The repr() representation is for programmers who can use it to construct the object by passing it to the eval method.

One point to remember is that the print() function automatically calls the str() function even if you don’t pass it as an argument to print. So, print(str(object)) is equivalent to print(object).

2.6 Everything is an object

The speciality of Python with regard to object orientation is that it treats everything as an object. As Python creator Guido van Rossum wrote, “One of my goals for Python was to make it so that all objects were ‘first class’. By this, I meant that I wanted all objects that could be named in the language (e.g., integers, strings, functions, classes, modules, methods, and so on) to have equal status. That is, they can be assigned to variables, placed in lists, stored in dictionaries, passed as arguments, and so forth.”

So, there are no built-in data types that are called ‘primitive data types’. All entities have some metadata called “attributes” and associated functionality called “methods”, which are accessed with the dot syntax.

The practical utility is that you can assign the “anything” to a “name” (variable) and use that variable to invoke available operations on it. For example, since functions are objects, you can assign your own convenient name to a function and call it. Another example is the list, a built-in data structure on which you can call the append method.

Even classes are instances of a meta-class “type” in Python and “type” is the only entity that is an instance of itself.

2.7 A peculiarity: the destructor

Python runs on its own virtual machine, the PVM. It generates intermediary byte code that is interpreted to the operating system. So one would normally think that like Java, it has no feature of destructor method and object memory would be recollected by the garbage collector. However, Python has a destructor. The function __del__() is called for any object when the reference count for that object becomes zero. It can also be invoked by calling the built-in function del() with the object. Programmers do not have to use the destructor. But they should be aware that this feature is present and they can explicitly remove objects from memory even before they are garbage collected, with the caveat that they do so properly keeping in mind object dependencies and circular references.

3. Example

charge.py

class Charge:
    MIN = 100
    charge_count = 0
    def __init__(self, charge_type, amount):
        print("Constructor getting executed")
        self.charge_type     = charge_type
        self.amount          = amount if amount > Charge.MIN else Charge.MIN
        print("Incrementing charge_count")
        Charge.charge_count  += 1 # can also be set via self.charge_count

    def __repr__(self):
        return f"Charge({self.charge_type!r}, {self.amount!r})"
  
    def __str__(self):
        return f"Charge Object, charge_type={self.charge_type}, amount={self.amount}"
    
    def __del__(self):
        Charge.charge_count -= 1
        print("Destructor completed.")
        
    @classmethod
    def how_many_charges(cls):
        return f"No. of charges = {Charge.charge_count}"
        
    
if __name__ == "__main__":
    print("Let's create an instance of Charge called chg")
    chg = Charge("Base", 10)
    print()
    
    print('*' * 20)
    print("Object chg's attributes are:")
    print(chg.charge_type)
    print(chg.amount)
    
    print("\nLet's check how many charges we have")
    print(Charge.how_many_charges())
    print()
    
    print('*' * 20)
    print("Now let's see class and object representations.")
    print("The default representation of class Charge: ")
    print(Charge)
    print()
    
    print("Invoking our customized repr() on 'chg', we get --")
    print(repr(chg))
    print()
    print("Calling print with object name invokes its str() method.\nHere it is for chg --")
    print(chg)
    print()
    
    print('*' * 20)
    print("We should be able to construct an object back\nwith the return value of repr()\n")
    print("Creating an object 'd' with repr and eval")
    d = eval(repr(chg))
    
    print("\nLet's check how many charges we have")
    print("but this time we'll call the class method on object 'd'")
    print(f"{d.how_many_charges()}")
    
    print()
    print("Let's set d's amount to 2000 and print it")
    d.amount = 2000
    print(d.amount)
    print()

The Charge class has two class variables, MIN and charge_count, which are initialized with values 100 and 0. There are two instance variables, charge_type and amount which need to be specified when instantiating the class. The purpose of MIN is to define the minimum amount a Charge should have. In the constructor, a check is made if the value of the second argument is less than 100. If it is, the attribute charge_amount is set to MIN.

The class attribute charge_count is used to keep a track of how many objects of this class are created. It is incremented by 1 in the constructor and decremented by 1 in the destructor. The class method how_many_charges is a convenient utility to display the charge_count.

We have customized the repr() method to return a string that can be passed to eval and create another object. We create a new object d, set its amount, and print the same. In the spirit of the str() intention to be useful to end-users, we return a string with some extra information.

Finally, before the program run ends, the destructor is called by Python for both the objects, chg and d.

The output of running charge.py is given in the screenshot below —

python class - output
Output of charge.py

4. Summary

This article has explained the concepts of classes and objects in Python. We have demonstrated their working with an example of using class and object variables, class and object methods, the constructor, and human-readable object representation.

5. More articles

6. Download the Source Code

Download
You can download the full source code of this example here: Python Classes and Objects

Last updated on Jan. 14th, 2022

Mahboob Hussain

Mahboob Hussain graduated in Engineering from NIT Nagpur, India and has an MBA from Webster University, USA. He has executed roles in various aspects of software development and technical governance. He started with FORTRAN and has programmed in a variety of languages in his career, the mainstay of which has been Java. He is an associate editor in our team and has his personal homepage at http://bit.ly/mahboob
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Ravi
Ravi
2 years ago

Well explained oops , constructor and destructor concepts

Back to top button