Swami Vivekananda

What are Python classes?

12 Apr 2022 - fubar - Sreekar Guddeti

python-Logo The organization of python program is discussed with class as an entity.

Abstract

Class is a high level unit of program organization. They facilitate a new kind of programming style called the object oriented programming (OOP) style. In contrast to the procedural programming style that constitutes functions, the OOP style constitutes classes where data is given precedence over flow of logic. This trade-off leads to new ways implementing behaviour and turns out to be more natural for certain use cases, for which procedural programming either has no solutions or the solutions it offer are cumbersome.

As it is a modern approach to progamming, it is pedagogical to compare and contrast classes with other structural units that Python offers like functions, modules. Analogies are also drawn with corresponding structures in natural language.

TL;DR

For the impatient, from the point of view of programming literacy, reading others code made up of classes is the first step. To read classes, we need to know its three aspects.

Writing code is the next step that is about hierarchial composition. The concept of inheritance allows a class to act as a either as superclass or subclass to another class. Syntactically, the superclass is provided as an argument to the class statement.

Introduction

We write programs to model reality. There are two complementary interpretations of reality; the reductionistic view that the whole is a sum (or composition) of its parts and the hierarchial view that the whole emerges from its parts Anderson1972. There are other interpretations that may be mimicked in writing programs but will not be considered here Wolfram1983, Poincare1854, न्याय 2nd century BC.

Irrespective of the interpretation, we need to identify the parts of the program and stitch them together. These parts are called program constructs - construct, in English, means to form by combining (con) parts or elements(struct) - and the process of identifying them is called program decomposition. Additionally, the way we stitch them defines the program architecture. Any architecture - program, civil or chip - is based on making design decisions that invariably introduce constraints Brown2011. Combined with these constraints, an architecture facilitates implementation of only a subset of reality. As long as we are confined to this subset, we may as well call this our reality, like the proverbial “frog in the well”. This confinement is called programming paradigm. In physics, a paradigm is also called a worldview. As explained in the “The Character of Physical Law”, Feynmann gives the example of how the Aristotlean paradigm of differing mechanics for the celestial and terrestrial objects was challenged by Newton and established a new worldview that both are governed by the same mechanics Feynmann1964. In the following, we describe the programming paradigms available at our disposal using the analogy of natural language.

Programming paradigms

Nouns and verbs predominantly constitute a natural language like English, Telugu or Hindi; the other elements of grammar play a supplementary role. In comparison with a programming language, if nouns denote the data, then verbs capture the logic or events.
To illustrate this comparison, let us model the canonical Ramayana epic. From an events perspective, the poem is famously summarized in Telugu language into just three verbs -

Ramayana summary: “కట్టే, కొట్టే, తెచ్చే”. which translates in English to “built, beat, brought”

So the summary is decomposed into three events– Rama built the bridge, Rama defeated Ravana, Rama brought back Sita home. Here, the characters and the bridge are the data.

The traditional programming languages like Fortran, C, provide procedural constructs like subprograms, subroutines or functions that help decompose program into events (or operations or actions). These constructs are essentially define the logic and are stitched together by the data i.e. data flows between them.

def build(character, object):
	"""
	Define logic that captures a character building an object.
	"""
	pass
	
def defeat(character1, character2):
	"""
	Define logic that captures a character1 defeating a character2.
	"""
	pass

def bring(character1, character2):
	"""
	Define logic that captures a character1 bringing back the character2 home.
	"""
	pass

build(Rama, bridge)
defeat(Rama, Ravana)
bring(Rama, Sita)

So, in what appears to be data gluing the logic, procedural constructs help implement the procedural programming paradigm that can also be called the event-oriented programming paradigm.

In the 1980’s, with the advent of the Ada programming language, new constructs like packages and tasks were introduced wherein the data is given prominence over logic Booch1986. Following Ada, the modern programming languages like C++, Java, Python refurbish these constructs into the class construct that packages both data and logic. A class generates what are called objects. In contrast to the flow of data in the procedural programming paradigm, classes interact with each other. To enable interaction between other objects, these have well defined object interfaces. The decomposition into objects that are stitched together by the object-interfaces they provide help implement the object-oriented programming (OOP) paradigm. Before we look at all the concepts of class in detail below, let us imagine an alternative summary of Ramayana. From the object oriented perspective, Ramayana could as well be summarized it into three nouns-

Ramayana summary: “Rama, Ravana, Sita”

The former summary is very relatable while the latter appears abstract. While the procedural decomposition might be more natural for systems that exhibit flat structure, the object-oriented decomposition, as we will soon see later, is ideal for systems that exhibit hierarchial structure.

The class construct greatly facilitates object-oriented design. It is least to say that to understand OOP is to understand what a class is. However, due to our natural tendency to think procedurally, the complementary programming paragdigm dealing with objects may not be grasped easily.

As a result, we gradually motivate the transition from procedural to object oriented paradigm by describing the class from the perspective of

As a construct, class acts as a placeholder of data and logic. As an object, class provides an interface to interact with other objects. As a generator, class provides a template to create objects that share the same quality. Finally as a namespace, class provides a flexible interface to alter its namespace using the property of inheritance. This is the most important feature of class that greatly improves code reusability and code customization.

Let us explore these features of class below.

Class as a construct

As a construct, a class packages or acts as a placeholder of data and logic. The data is defined by the attributes an

Moreover, a class itself is a name with underlying names termed as its attributes. So a class consists of data and method attributes. As a structural element, a classes encapsulate data and logic using the class construct.

Syntactically, the class construct is a class statement. A class statement is a block statement similar to the def statement that defines a function.

class MyClass:
	data_attributes
	method_attributes(self, ):
		...
		...	

The class acts as a placeholder of its constituent names. Since it is also an object it can be assigned to a variable as

my_variable = MyClass()

Also it can be passed as an argument to a function like other built-in objects.

my_function(MyClass)

Style Tip: As per the PEP 8 style guide, it is recommended to use CapitalizedWords naming style for class names.

Data attributes

As discussed earlier, data attributes define the state of the class or an instance of the class. To access the state attributes from without the class, we use the object.attribute syntax again. Here the object might be the class itself or the instance of the class. To access the state from within the class, we use the self.atttribute syntax

Method attributes

In the general sense, a function interface allows flow of external data. However, the method attribute, which itself is a function, needs to acccess data that is internal to the object interface. To allow for this, the function interface in the special case of a method attribute is extended by allowing a special argument called self that references the object of which the method is part of. This is called self-referencing and graphically visualized below.

extension-of-function-interface-in-class

Class as an object

Since we posited earlier that class is itself a name, a natural question arises; whether a class is of type data or logic?. To answer this question, let us transcend from the above categorization of names and introduce the concept of an object.

An object is an abstraction that encapsulates names.

Names of type data denote the state of the object, while names of type logic determine its behaviour by performing operations upon the data.

The verb encapsulate, in English means, to contain something; it also means to hide something like a black box. Accordingly, objects serve two purposes-

While the first connotation enables name encapsulation, the second connotation facilitates interaction between objects.

Name encapsulation

As a programming paradigm, this name encapsulation within a single structure is termed object-oriented programming (OOP).

In contrast, functions encapsulate logic, whereas modules hold both data and logic. So from the above definition of an object, functions and modules are also objects. Even the built-ins that constitute functions and modules are objects.

Illustration: Joker class

joker-dc-comics-image

Joker of DC comics. Image taken from Wikipedia.

To illustrate the class construct, let us consider the example of the Joker character from DC comics. While many actors have played the role fo Joker, as a character, there is one-and-only-one Joker. Let us model the Joker as a class of his own. As his attributes, the first thing that comes to the mind are his name and his quotes. While he delivers a context-dependent dialogue, for pedagogical purposes, let us model the behaviour that delivers a random quote.

class Joker:
    name = 'The Joker'
    quotes = ['Why so serious?',
              'I just doooo things.',
              'Do I look like I am man with a plan?'
             ]
    def tell_random_quote(self):
        n_quotes = len(self.quotes)
        from random import randint
        quote = self.quotes[randint(0, n_quotes-1)]
        return quote

While the data attributes are self-explanatory, the method attribute, which are essentially functions, contains an additional special argument self that has been encountered for the first time. Before we explain the self argument, let us first see how to access the encapsulated attributes across the object’s interface.

Object-to-object interaction

From above, it is clear that name and quotes are data attributes, whereas random_quote() is a method attribute.

One way of looking at attributes is to think of them as properties. However, an equally valid way of looking at them is to think of them defining the object.

Both these ways are expressions of the object-attribute relation. In essence, we are dealing with relations. In the former way, the object takes precedence, wherease in the later, the attributes take precedence. If we work with the later way, then the data attributes define the state of the object, whereas the method attributes action performed by the object and on the object. The sum total of the state and actions define the behaviour of the oject.

In any case, unless there is access to the attributes of the object, other objects cannot interact with it, thereby an object is of no use in implementing a model of reality. Python encourages object-to-object interaction using the object.attribute expression that provides an interface to access and modify the behaviour of the object.

Illustration: Access Joker’s behaviour

To access Joker’s name, we use Joker.name expression that gets the name attribute of Joker class.

print(Joker.name)

prints

The Joker

If we additionally want to listen to a random Joker quote, we use Joker.tell_random_quote(Joker) expression.

print(Joker.tell_random_quote(Joker))

prints

I just doooo things.

The above expression in natural language means “call the tell_random_quote() method attribute of Joker class object that takes as an argument the object itself and return one of his random quotes to print to the standard output.”

There are two features in this expression

To summarize

Python is a collection of interacting objects, each of whose attributes can be accessed by the object.attribute expression.

Class as a generator

We posited earlier that Joker is in a league of his own. As an abstraction, this is a true statement. However, there have been more than one attempt by Hollywood directors to portray this character. Naturally, the definition of the Joker is limited by the imagination of the director, the ability of the actor and probably the era during which the movie was released.

There are atleast three famous portrayals of The Joker:

respectively during the golden, silver and modern era. Therefore, in reality, there are more than one Jokers that share the same “Jokerness”, but are only different as far as “degree of Jokerness” is concerned Vivekananda.

In OOP parlance, objects that share the same ‘ness’ but are only different as far as the “degree of ‘ness’” is concerned are called instances.

To capture the shared “Jokerness”, while at the same time delineate the differences in the “degree of Jokerness”, the class construct defined earlier is not sufficient. Instead, we extend the class construct with the addition of the __init__() method attribute.

class my_class:
	__init__(self):
		...
		...
	data_attributes
	method_attributes(self, ):
		...
		...

With the use of the __init__() method, we define a new Reel_Joker class that has the version attribute, in addition to the attributes already defined in the Joker class. The version denotes the era the movie was released in, the director and the actor that played the role of The Joker. Let us assign these three features as a dictionary to the version attribute.

class Reel_Joker:
    name = 'The Joker'
    quotes = ['Why so serious?',
              'I doooo things.',
              'Do I look like I am man with a plan?'
             ]
    def __init__(self, age, director, actor):
        self.version = dict(	age=age,
					director=director,
					actor=actor
				)
    def tell_random_quote(self):
        n_quotes = len(self.quotes)
        from random import randint
        quote = self.quotes[randint(0, n_quotes-1)]
        return quote

Before we explain the init(), let us first define the different instances of Joker class.

Instance of a class

The process of defining a new instance is called instantiation.

We can assign an instance to a name using the assignment statement.

joker_golden = Reel_Joker("golden", "Tim Burton", "Jack Nicholson")
joker_silver = Reel_Joker("silver", "Christopher Nolan", "Heath Ledger")
joker_modern = Reel_Joker("modern", "Todd Phillips", "Joachim Phoenix")

init() method

Instantiation needs external data. For each of the above three unique instances, we pass the age, director and actor as external data. These data are passed to the special method __init__() that performs the role of instantiation. In addition to self, __init__ takes the arguments age, director and actor and respectively assigns to 'director', 'actor' and 'age' keys of version attribute. To define version as an attribute of the class, self.version expression is used.

If the Reel_Joker class is abstracted graphically as a Circle class, then the above three instances can be considered to be circles of differing degrees and center coordinates as shown below.

class-instance-relationship

To summarize,

In the OOP parlance, we say that a class generates new instances of objects.

An additional feature in the above figure is the directed connectors to denote that the instance objects inherits attributes of the corresponding class object as discussed below in the section on class inheritance. But first, let us discuss the instance interface.

Instance interface

Since instances are also objects, their attributes can be fetched using the usual object.attribute expression.

golden_age_actor = joker_golden.version.get('actor')
silver_age_director = joker_silver.version.get('director')

print(f'{joker_golden.name} of the golden age was played by {golden_age_actor}.')
print(f'{joker_silver.name} of the silver age was directed by {silver_age_director}.')

gives the result

The Joker of the golden age was played by Jack Nicholson.
The silver age Joker was director by Christopher Nolan.

While the instance and the corresponding class are different objects, as discussed below in the section on class inheritance, they share the same interface because an instance inherits the attributes of the corresponding class.

Class as a namespace

In the most general sense, a class is a namespace, a space of names.

Names in a programming language are broadly categorized into two types

Namespace extension by inheritance

In the section on self argument, we have seen how self argument allows a class to self-reference its own attributes, and in the section on init construct, we have seen how __init__() method spawns new instances of a class. Now let us explore another inheritance, another powerful feature of the class construct.

As discussed earlier, the attributes of a class define it. If another class’s definition has the same attributes as the former in addition to a new set of attributes, then it is possible to define the later by inheriting the former’s attributes and only defining the new set of attributes. The process of sourcing one class’s attributes to the other is called inheritance. The class that sources its attributes is called superclass, and the class that inherits the attributes is called subclass.

To capture the inheritance, the class-as-a-generator construct defined earlier is not sufficient. Instead, we extend the class-as-a-generator construct to also accept superclasses as arguments.

class my_subclass(my_superclass):
	__init__(self):
		...
		...
	data_attributes
	method_attributes(self, ):
		...
		...

With the passing of superclass as an argument, we can inherit the attributes of the superclass into the sub-class

class Inherited_Joker(Joker):
    def __init__(self, age, director, actor):
        self.version = dict(	age=age,
					director=director,
					actor=actor
				)

Notice that we have passed the Joker class as an argument to the Inherited_Joker class.

The above class statement in natural language means “the Inherited_Joker class inherits the attributes of Joker class. In addition, it has an __init__ method attribute.”

Since the __init__ defines the version attribute, the complete list of attributes of Inherited_Joker class is

Similarly, another class may source its attributes from the Inherited_Joker class. From this it is evident that there is a hierarchy

In OOP parlance, a class inherits attributes from its superclasses and sources attributes to its subclasses.

Inheritance interface

We can immediately see two-fold utility in the inheritance interface of class.

For classes

Inheritance adds depth to the abstraction. Inheritance allows efficient reuse of code.

In contrast, functions lack such an inheritance interface. As a result functions are useful when namespace extension is not needed. On the other hand, import statements can extend namespaces of modules and packages. For example, to incorporate the names of a random module into the top level script file, we use either of the following statements

import random
import random as rd

The randint() method can then be accessed by the usual object.attribute expression as

random.randint()
rd.randint()

While this is convenient, the namespace extension needs explicit execution of the import statement. However, in the case of classes, it is implicit as the object.attribute expression initiates an automatic tree search for the attribute across the inheritance hierarchy tree.

For instances

In the Python object model, an instance and its corresponding class are two different objects. However, an instance inherits the attributes of the class. For example, the name attribute of both the Reel_Joker class and any of its instance, say joker_golden instance object are one and the same.

print(Reel_Joker.name == joker_golden.name)

prints

True

To summarize,

In OOP parlance, an instance inherits the attributes of the class that generates it.

For mutated subclasses

It also allows mutations of the attributes leading

To summarize,

Inheritance is the flaship property of object-oriented programming that offers efficient code reuse, extension of namespaces, spawning multiple instances and code customization.

Namespace alteration

References

All differences are not that of a kind but only of a degree – Swami Vivekananda.

Assets