Introduction

In their basic form, classes are easy to understand. In fact, classes have just three primary distinctions from the rest of Python syntax. At a base level, they are mostly just namespaces, much like modules … however, unlike modules classes also have support for:

  • Generating multiple objects.
  • Namespace inheritance.
  • Operator overloading.

Overview

Class objects

A class, in programming terms, is itself a Python object. But in layman terms you can think of it as a cookie cutter, or a factory; that is, a template from which many (instance) objects can be created.

Instance objects

The individual objects, created by class objects, are known as instances … when you call a class object, you get an instance object.

Class objects create instance objects

Class objects come from statements, and instances come from calls; thus each time you call a class, you get a new instance of that class. Thus class objects provide the default behaviour and serve as factories for instance objects … they are able to generate multiple instance objects.

Instance objects are the real, i.e. the actual, objects your programs process.

  • Each is effectively a namespace in its own right, but inherits (i.e., has automatic access to) names in the class from which it was created.
  • Each instance object can have its own, independent data … underscoring the support of multiple versions (of the instance object) from a single class object.

Quick summary

  • Classes define common, shared data and behaviour, and generate instances. Instances reflect concrete application entities, and record per-instance data that may vary per object.
  • Classes define a namespace - a scope of state explicitly restricted to their curly braces. They also support customization by inheritance and operator overloading.
  • Finally, since class objects can contain custom methods this allows them to support multiple additional custom behaviours.

A class can generate multiple objects

Class objects provide the default behaviour

  1. The class statement creates a class object and assigns it a name: So this is analogous to defining a function with the def statement … both class and def are executable statements, which when reached and run by the Python interpreter generate a new object and assigns it to the given name.

  2. Assignments inside a class statement make class attributes: Strictly speaking the class statement defines a local scope (a namespace) such that attributes defined within the class object are contained within this namespace. Thus, after running the class statement the class attributes can be accessed by prefixing the attribute name with the class name. For example MyClass.counter.

  3. Class attributes provide object state and behaviour: Attributes of a class object maintain state information and behaviour … which is then shared by all instance objects created from the class.

Instance objects are individual items

  1. Calling a class object makes a new instance object: Each time a class is called, it creates and returns a new instance object. Instances represent concrete items in your program’s domain.

  2. Each instance object inherits class attributes and gets its own namespace. Instance objects created from classes are new namespaces; they start out empty but inherit attributes that live in the class objects from which they were generated.

  3. Assignments to attributes of self in methods make per-instance attributes. Inside a class’s method functions, the first argument (called self by convention) references the instance object being processed; assignments to attributes of self create or change data in the instance, not the class.

Examples

class MyClass:
    def setData(self, value):
        self.data = value

    def display(self):
        print(self.data)

As usual, the above statement would only be run either when the module file it is coded in is imported … or when the Python interpreter reaches the code. Thus the class won’t even exist until Python reaches and runs this statement.

Like all compound statements, the class starts with a header line that lists the class name, followed by a body of one or more nested and indented statements. The body of the class is contained within the curly braces.

Function definitions, using the def keyword, can be included within the class object - although in the context of class objects they are usually referred to as class methods. They define and implement the behaviour we want the class to export. Here, the methods setdata and display are part of the class statement’s scope/namespace. Thus we access them from outside of the class by means of the dot syntax: MyClass.setdata and MyClass.display.

Class methods (that is functions defined with def) support all syntax and implementation details of regular functions … for example defaults, return values, yield items on request, etc., etc. However, in a class method, the first argument automatically receives an implied instance object when called … that is, they receive the subject of the call.

We can create multiple instance objects, from the class object, as follows:

instance_a = MyClass()
instance_b = MyClass()

In object-oriented programming terminology, we say that instance_a “is a” MyClass (object).

Then, we can call the class methods as follows:

instance_a.setdata("Hello world")
instance_b.setdata(3.1459)

And you can see how we can qualify an instance object with the name of an attribute that is defined in the original class object. Python follows the link from the instance_a instance object, up the scope chain to the MyClass class object. Once there it will find the setdata method and associates the data provided with the specific instance object that began the call.

Within the setdata method, the self argument will refer to the instance being processed (either instance_a or instance_b). In turn, the assignment of data will store the value in the instance’s namespace … not the class’ namespace.

Finally, notice how we can store different data types in the data attribute of the instance object. No declaration of data is needed within the instance object, beyond that which occurs the first time they are assigned values … making the just like regular simple Python variables.

Instance objects are dynamic … that is, as well as storing different data types into the same class attribute, instance objects can also change their attributes dynamically. For example:

  • With instance_a="New value" we can dynamically change the data attribute of the instance_a member.
  • Moreover, if we needed to, we could introduce an entirely new instance member within the instance’s namespace by using dot notation and assigning the attribute to the name of the instance object. For example: instance_a.my_new_attribute="Goodbye".

Classes are customized by inheritance

Inheritance allows for specialization

Classes also allow us to make changes by introducing new components (called subclasses), instead of changing existing components in place. In the same way that instance objects generated from a class inherit the class’s attributes … so too can classes inherit from other classes.

In turn, this allows for the creation of coding hierarchies of classes that (with each generation) further specialize behaviour. By redefining attributes in subclasses that appear lower in the hierarchy, we override the more general definitions of those attributes higher in the tree.

Effectively, the further down the hierarchy of subclasses that we go, the more specific the classes become. Instance objects inherit from class objects … class objects inherit from superclasses.

In this way class inheritance allows us to customize what already exists, rather than chaning it in place and/or starting from scratch. Thus minimizing code redundancy and keeping our code DRY (don’t repeat yourself).

Inheritance Examples

Define a superclass

class MyFirstClass:
    def setData(self, value):
        self.data = value

    def display(self):
        print("From the superclass: ", self.data)

Then define a subclass that inherits the setdata method from the superclass, but also replaces the display method with it’s own implementation. And presumably the implementation of display in the MySecondClass subclass would be more specific than that in the MyFirstClass superclass.

class MySecondClass(MyFirstClass):
    def display(self):
        print("From the subclass: ", self.data)

The search for an attribute, method, or member begins at the instance object on which the request was made. It then proceeds upwards through the hierarchy to subclasses - then to superclasses - before stopping at the first appearance of the attribute name that it finds.

So in the example above, calling the display method will invoke the implementation found in MySecondClass because it will be found before the one in MyFirstClass. We say that MySecondClass overrides MyFirstClass, in the implementation of display. MySecondClass.display is said to overload MyFirstClass.display.

And thus MySecondClass.display can introduce specialization that is completely external to MyFirstClass. Thus any existing or future MyFirstClass instance objects will make use of MyFirstClass.display.

Now, to be clear, in the case of setData both MyFirstClass and MySecondClass will still refer to the setData method found in the MyFirstClass class object. But what this will mean, if we call setData from a MySecondClass instance object, is that the class method (setData) used to set the instance member (data) will come from MyFirstClass … whereas the method used to print the data member will come from MySecondClass. Therein, the notion that inheritance allows for specialization can be seen in whatever custom message the display method of the MySecondClass class object provides.

Inheritance Summary

Superclasses are listed in parentheses in a class header

  • To make a class inherit attributes from another class, just list the other class in parentheses in the new class statement’s header line.
  • The class that inherits is usually called a subclass, and the class that is inherited from is its superclass.

Classes inherit attributes from their superclasses

  • Just as instances inherit the attribute names defined in their classes, classes inherit all of the attribute names defined in their superclasses; Python finds them automatically when they’re accessed, if they don’t exist in the subclasses.

Instances inherit attributes from all accessible classes

  • Each instance gets names from the class it’s generated from, as well as all of that class’s superclasses. When looking for a name, Python checks the instance, then its class, then all superclasses.
  • Python performs an independent search of the class tree for each attribute fetch expression. This includes references to instances and classes made outside class statements (e.g. X.attr), as well as references to attributes of the self instance argument in a class’s method functions. Each self.attr expression in a method invokes a new search for attr in self and above.

Logic changes are made by subclassing, not by changing superclasses

  • By redefining superclass names in subclasses, lower in the hierarchy, subclasses replace and thus customize inherited behaviour.

Classes can overload Python operators

Overloading allows us to intercept and redefine Python built-in types

In simple terms, operator overloading enables instance objects to intercept and respond to operations that work on built-in types e.g. addition, slicing, printing, qualification, etc., etc. Python expressions and other built-in operations automatically route control to implementations in classes.

Although we could implement all the class behaviour we might ever need as method functions, operator overloading lets objects be more tightly integrated with Python’s object model. Moreover, because operator overloading makes our own objects act like built-in objects, it tends to foster object interfaces that are more consistent, and it allows class-based objects to be processed by code written to expect a built-in type’s interface.

The operator overloading method that you are most likely to see is the __init__ method, which is known as the constructor method and is used to initialize objects’ state. You should pay special attention to this method, because __init__, along with the self argument, turns out to be a key requirement to reading and understanding most OOP code in Python.

Overloading Summary

Methods named with double underscores are special hooks

  • In Python classes we implement operator overloading by providing specially named methods to intercept operations. The Python language defines a fixed and unchangeable mapping from each of these operations to a specially named method.

Such methods are called automatically when instances appear in built-in operations

  • For instance, if an instance object inherits an add method, that method is called whenever the object appears in a + expression. The method’s return value becomes the result of the corresponding expression.

Classes may override most built-in type operations

  • There are dozens of special operator overloading method names for intercepting and implementing nearly every operation available for built-in types. This includes expressions, but also basic operations like printing and object creation.

There are no defaults for operator overloading methods, and none are required

  • If a class does not define or inherit an operator overloading method, it just means that the corresponding operation is not supported for the class’s instances. If there is no add, for example, + expressions raise exceptions.

Operators allow classes to integrate with Python’s object model

  • By overloading type operations, the user-defined objects we implement with classes can act just like built-ins, and so provide consistency as well as compatibility with expected interfaces.

Operator Examples

Define a superclass

class MyFirstClass:
    def setData(self, value):
        self.data = value

    def display(self):
        print("From the superclass: ", self.data)

Then define a subclass:

class MySecondClass(MyFirstClass):
    def display(self):
        print("From the subclass: ", self.data)

And finally define a further subclass:

class MyThirdClass(MySecondClass):
    def __init__(self, value):
        self.data = value

    def __add__(self, other):
        return MyThirdClass(self.data + other)

    def __str__(self):
        return "Some sort of string: " + self.data

    def multi(self, other):
        self.data *= other

Upon instantiation, myInstance will call the __init__ instructor, with A string being passed to the value argument. Therein, the data member of myInstance instance object is set automatically, rather than in the case of instance objects created with either MySecondClass or MyFirstClass, which would require explicit calls to the setData method.

myInstance = MyThirdClass("A string")

As before, Python will climb the scope chain to find the first implementation of a method. Thus in this case myInstance instance object would be calling the MySecondClass.display method.

myInstance.display()

Note here how the __add__ method makes and returns a new instance object of its class. This is because it calls the MyThirdClass class object, which in turn calls the __init__ method to initialize the result. Beyond that, it is simply the exisitence of the + that will trigger Python to call the __add__ method.

another_instance = myInstance + "another string"

Finally we can contrast __add__ to multi in that the latter doesn’t make and return a new instance object. Instead, it changes the current instance object in place by reassigning the self attribute.

Classes are Attributes in Modules

When it comes to using class objects we can think of them as if they were like any other variable $\dots$ that is, a variable assigned to an object when the class statement runs. Likewise the instance object can be referenced with any normal expression.

Some examples:

  • Coding a class object in a module file of it’s own:
  from <module-name> import MyFirstClass
  class MySecondClass(MyFirstClass):
      ...
  • Multiple class objects defined in a single module file:
class Car:
    ...
class Truck:
    ...
class Bus:
    ...
  • Namespace resolution allows complex names:
# In a file called person.py
class Person:
    ...
# Some other file.py
import Person
x = person.Person()
  • And as always, a module represents an entire file … a class represents a statement within a file.

Conclusions

CLASSES AND MODULES

  • Classes are always nested inside a module; they are attributes of a module object.
  • Classes and modules are both namespaces, but classes correspond to statements (not entire files) and support the OOP notions of multiple instances, inheritance, and operator overloading (modules do not).
  • In a sense, a module is like a single-instance class, without inheritance, which corresponds to an entire file of code.

INSTANCES AND CLASSES

  • Classes are made by running class statements; instances are created by calling a class as though it were a function.

CLASS ATTRIBUTES

  • Class attributes are created by assigning attributes to a class object.
  • They are normally generated by top-level assignments nested in a class statement—each name assigned in the class statement block becomes an attribute of the class object (technically, the class statement’s local scope morphs into the class object’s attribute namespace, much like a module). Class attributes can also be created, though, by assigning attributes to the class anywhere a reference to the class object exists—even outside the class statement.

INSTANCE ATTRIBUTES

  • Instance attributes are created by assigning attributes to an instance object. They are normally created within a class’s method functions coded inside the class statement, by assigning attributes to the self argument (which is always the implied instance). Again, though, they may be created by assignment anywhere a reference to the instance appears, even outside the class statement. Normally, all instance attributes are initialized in the init constructor method; that way, later method calls can assume the attributes already exist.

self

  • self is the name commonly given to the first (leftmost) argument in a class’s method function; Python automatically fills it in with the instance object that is the implied subject of the method call. This argument need not be called self (though this is a very strong convention); its position is what is significant. (Ex-C++ or Java programmers might prefer to call it this because in those languages that name reflects the same idea; in Python, though, this argument must always be explicit.)

OPERATOR OVERLOADING

  • Operator overloading is coded in a Python class with specially named methods; they all begin and end with double underscores to make them unique.
  • These are not built-in or reserved names; Python just runs them automatically when an instance appears in the corresponding operation.
  • Python itself defines the mappings from operations to special method names.
  • The special self argument in method functions and the init constructor method are the two cornerstones of OOP code in Python and the two whose use is most widespread.