OOP#

Object-Oriented Programming (OOP) is a programming paradigm that provides a way to structure code by organizing it into objects. Objects are instances of classes, which are blueprints that define the behavior and characteristics of the objects. Python is an object-oriented programming language that fully supports OOP principles.

For more information check:

  • Data model section of the official documentation.

  • Classes tutorial in the official documentation.

Objects#

Everything in python is an object. Each object has it’s unique identifier which you can load using the id build-in function. The is operator allows you to check if two names refer to the same object.


The following cell defines the int object and shows it’s id.

value = 10
id(value)
99205780812584

Now for thevalue assigned to value2. id(value2) is the same as id(value) - they’re actually the same objects.

value2 = value
id(value2)
99205780812584

This can also be checked with the is operator.

value is value2
True

But if you assign a different literal to value2 - a new object will be created under that name.

value2 = 30
id(value2)
99205780813224
value is value2
False

Variables#

Classes and their instances can contain variables (sometimes called data attributes) - it’s a peace of data that corresponds to the class or its instances.

There are few important concepts you need to know about “data attributes”:

  • There are attributes defined for whole class and attributes unique for each instance.

  • There are special dynamic attributes that during operation wite class behaves like a regular data attribute, but in real it’s a method - so you can compute value of the attribute dynamically.

Find out more accurate description in the corresponding page.


The following cell defines class where:

  • class_var: is a class variable.

  • instance_var: is a variable that will correspond to each instance of the class.

  • dynamic_attribute: is an attribute whose value is counted at the moment of reference to it.

class MyClass:
    class_var = 10

    def __init__(self):
        self.instance_var = 45

    @property
    def dynamic_attribute(self):
        return self.instance_var + 7

Private attributes#

There is just a convention in the python community - to consider attributes starting with underscore (e.g. _spam) as a private part of the API, but there are no mechanisms that prevent you from using/modifying it.

There is only one mechanism to prevent duplicate names during inheritance. If you define an attribute with a name that starts with two underscores like __spam, python will automatically create another reference to that attribute with name that follows pattern: _<name of the class>__<name of attribute>.


The following cell creates an attribute that has a method which name starts with double underscore: __private_method. But from instance of the MyClass it calls __MyClass__private_method.

class MyClass:
    def __private_method(self):
        print("Private method from MyClass")

my_class = MyClass()
my_class._MyClass__private_method()
Private method from MyClass

As a result, the program behaves exactly as it was declared in the _private_method.

The following cell creates a subclass for MyClass and shows that even if you reassign __private_method, extra reference automatically created by the python _MyClass__private_method still exits.

class MySubClass(MyClass):
    def __private_method(self):
        print("Private method from MySubClass")

my_sub_class = MySubClass()
my_sub_class._MyClass__private_method()
Private method from MyClass

And behaves as it declared in the MyClass.__private_method.

Inheritance#

One of the key features of OOP is inheritance, which allows a class to inherit attributes and methods from another class. The class that inherits is called a subclass or derived class, and the class from which it inherits is called a superclass or base class.

Check details on features of the inheritance in the corresponding page.


The following cell defines the Car class, which implements the general car, and creates the ElectricCar subclass, which inherits all the properties from the Car class, but adds properties specific to the electric car.

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        
    def start(self):
        print("The car has started.")
        
    def stop(self):
        print("The car has stopped.")


class ElectricCar(Car):
    def __init__(self, make, model, year, battery_capacity):
        super().__init__(make, model, year)
        self.battery_capacity = battery_capacity
        
    def charge(self):
        print("The electric car is charging.")

In this example, we define a subclass ElectricCar that inherits from the Car class. It has an additional attribute battery_capacity and a new method charge. The super() function is used to call the superclass’s constructor and initialize the inherited attributes.

my_electric_car = ElectricCar("Tesla", "Model S", 2023, 100)
print(my_electric_car.make)
print(my_electric_car.battery_capacity)
my_electric_car.start()
my_electric_car.charge()
Tesla
100
The car has started.
The electric car is charging.

Here, my_electric_car is an object of the ElectricCar class, which inherits attributes and methods from the Car class. It also has its own specific attributes and methods.

Special methods#

In classes, a set of methods with reserved names can be implemented. These methods are called when specific events occur with the class or its instances. Names of all this methods follows such pattern __<name>__, for example __init__, __repr__ and so on.

Find out more in the:

The following table shows some special methods and their descriptions.

Group

Method

Description

Object management

__new__(cls)

Static method called to create a new instance

__init__(self)

Called immediately after instance creation

__del__(self)

Called when the instance is about to be destroyed - the correct abstract way to name it is “finalizer”

Type Conversion

__str__(self)

Defines how an object should be converted into a string datatype.

__bool__(self)

Defines how an object should be converted into a boolean datatype.

__int__(self)

Defines how an object should be converted into an integer datatype.

__float__(self)

Defines how an object should be converted into a float datatype.

__complex__(self)

Defines how an object should be converted into a complex datatype.

Arithmetic operators

__eq__(self, other)

Compares two objects for equality (==).

__lt__(self, other)

Compares if the object is less than another (<).

__le__(self, other)

Compares if the object is less than or equal to another (<=).

__gt__(self, other)

Compares if the object is greater than another (>).

__ge__(self, other)

Compares if the object is greater than or equal to another (>=).

__add__(self, other)

Defines addition for objects (+).

__sub__(self, other)

Defines subtraction for objects (-).

__mul__(self, other)

Defines multiplication for objects (*).

__truediv__(self, other)

Defines division for objects (/).

__floordiv__(self, other)

Defines floor division for objects (//).

__mod__(self, other)

Defines modulo operation for objects (%).

__pow__(self, other)

Defines power operation for objects (**).

__and__(self, other)

Defines bitwise AND operation (&).

__or__(self, other)

Defines bitwise OR operation (`

__xor__(self, other)

Defines bitwise XOR operation (^).

__iadd__(self, other)

Defines in-place addition (+=).

__isub__(self, other)

Defines in-place subtraction (-=).

__imul__(self, other)

Defines in-place multiplication (*=).

__idiv__(self, other)

Defines in-place division (/=).

__neg__(self)

Defines unary negation (-).

__abs__(self)

Returns the absolute value of the object (abs()).

Indexing operator ([])

__setitem__(self, key, value)

To try assigning a value to the index.

__getitem__(self, key)

To try to access value under the index.

__delitem__(self, key)

To try to delete value under the index

Iteration protocol

__iter__

Will be called each time you need iterate over object.

__next__

Will be called each time the object is passed to the next build in function.


As an example, consider a class that has the __getitem__ method defined. This method determines the behavior of the instances of the class when the [] operator is applied to them.

Here is how it works — it converts the literal 3 to the type of the input and applies + to the input and the transformed literal 3.

class TestClass:
    def __getitem__(self, item):
        return item + type(item)(3)

The following cell shows the behavior of the instance when 9 is passed to the [] operator.

TestClass()[6]
9

You can pass string literals as well.

TestClass()["hello"]
'hello3'

Special attributes#

Actually, almost every instance in the Python environment has its own special attributes, but they come from different abstractions of the language:

  • Type attributes: Attributes of the type itself. The type class, like int, holds these attributes.

  • Function attributes, Method attributes: Their purpose is clear from their names.

  • Instance attributes: Attributes that appear in an instance during its creation.

Category

Attribute

Description

Type Attributes

__name__

Name of the class.

__module__

Module where the class is defined.

__dict__

Dictionary containing the class’s attributes and methods.

__bases__

Tuple of base classes (superclasses).

__mro__

Method Resolution Order (tuple of base classes in order).

__subclasses__()

Returns a list of known subclasses.

__doc__

Docstring of the class (or None if not defined).

__annotations__

Dictionary of variable type annotations.

__qualname__

Fully qualified class name (e.g., Outer.Inner).

Function Attributes

__code__

Code object representing the function’s compiled bytecode.

__defaults__

Tuple of default values for function parameters.

__kwdefaults__

Dictionary of default values for keyword-only arguments.

__globals__

Reference to the global namespace where the function is defined.

__closure__

Tuple of cell objects containing variables used in closures.

__annotations__

Dictionary of function parameter and return type annotations.

Method Attributes

__func__

Reference to the underlying function of a method.

__self__

Instance to which a bound method is attached.

__doc__

Docstring of the method.

__annotations__

Dictionary of method parameter and return type annotations.

Instance Attributes

__dict__

Dictionary containing instance attributes.

__class__

Reference to the class of the instance.

__slots__

Tuple defining allowed instance attributes (if used).

__weakref__

List of weak references to the object (if applicable).

Check more specific description at the corresponding page.

Class method#

A class method is a method that takes the class itself as its first argument (typically named cls). It should be defined using the classmethod decorator. A crucial feature of a class method is that it can be called not only from an instance of the class (like a typical method) but also directly from the class itself.


The following cell defines class_method, with its name reflecting its properties. In this case, class_name returns cls, allowing us to verify what it represents.

class ClassMethodExample:
    @classmethod
    def class_method(cls):
        return cls

The following two cells use class_method from the class itself and from the instance of the class.

ClassMethodExample.class_method()
__main__.ClassMethodExample
ClassMethodExample().class_method()
__main__.ClassMethodExample

The following cell proves that cls is exactly the object of the class.

ClassMethodExample.class_method() == ClassMethodExample
True

Static method#

Static method is a method of the class that isn’t bound to any object - I like to think of it as an about-usual function, but just in a class namespace. Such an approach allows creating functions that are logically associated with a class but can be called without an instance. Check out more about static methods in the corresponding tutorian on the digital ocean.

You can formally define a method as static by wrapping it in the staticmethod decorator. Note: A static method doesn’t have any relation to the instances of the object, which is why it shouldn’t have the self parameter.


The following cell implements a class that contains a regular method and a static method.

class StaticExample:
    def typical(self):
        print("I'm typical.")

    @staticmethod
    def static():
        print("I'm static.")

Static methods are easily accessed by <class name>.<method name>. The following cell shows it:

StaticExample.static()
I'm static.

The same approach with a non-static typical method will result in a corresponding error.

try:
    StaticExample.typical()
except Exception as e:
    print(e)
StaticExample.typical() missing 1 required positional argument: 'self'

Abstractions#

Abstract class is a class which instance can’t be created. Typically it is used to define rules for defining children of the class - from these child classes instances of the class can be created. To define an abstract class, you must create it as a child of the abc.ABC and define methods there that must be overloaded in children classes.

Check more details in:


The following cell defines an abstract class with an abstract_method method that must be overloaded.

from abc import ABC, abstractmethod

class AbstractClass(ABC):
    @abstractmethod
    def abstract_method(self):
        pass

The following code shows that you can’t create an instance of the AbstractClass.

try: AbstractClass()
except Exception as e: print(e)
Can't instantiate abstract class AbstractClass without an implementation for abstract method 'abstract_method'

The purpose of the AbstractClass is to be an ancestor for other classes and to define methods that need to be overloaded.

class Ancestor(AbstractClass):
    def abstract_method(self):
        print("Implemented abstract method.")

ancestor = Ancestor()