How To Write Objects that Function the same as those in Python's

How To Write Objects that Function the same as those in Python's

Write objects that function the same as those in Python's standard libraries. In this article, we discuss how to utilize built-in functions like _len_ and _eval_ to make objects in Python that make use of the language's data.

Before we start exploring how to write a Pythonic Object, let us start by making it clear what I mean by that term. It is not about PEP8 and respecting its rules to write beautiful pythonic code; rather it's about writing objects that make maximum use of the concepts of the Python data model, so they can be used as naturally as the Python standard library objects.

The idea is to inject Python ADN in our user-defined objects to make them mutate and behave as native Python objects. To do so, we will implement a Vector class to represent a multidimensional vector.

The code below represents the Vector class with its minimal implementation. A Vector is represented by its coordinates. 

from array import array

class Vector:

    __arrayType = "d"

    def __init__(self, coordinates):
        self.__coordinates = array(self.__arrayType, coordinates)

if __name__ == "__main__":
    v = Vector([1, 2, 3])
    print(v)
    #<__main__.Vector object at 0x0082F610>
    v1 = Vector((1, 2, 3))
    print(v1)
    #<__main__.Vector object at 0x0317FC88>

The Vector coordinates are stored in a float array; notice the  __arrayType = "d" that imposes the type of elements within the array to floats. We can pass any iterable to the constructor of Vector since the constructor of the array uses as an internal container that accepts any _iterable _(tuples, lists, etc.).

A Pythonic Representation 

When we print a Vector object itself, note that we get its reference (memory address with CPython) and not its coordinates. Let us change that by implementing the __str__ method within our class to have a more friendly output like for example  (x, y, z, ..).

from array import array

class Vector:

    ....

    def __str__(self):
        return str(tuple(self.__coordinates))

    ....

if __name__ == "__main__":
    v = Vector([1, 2, 3])
    print(v)
    #(1.0, 2.0, 3.0)

Automatically, when the print is called with a Vector object, the_ __str__ method is executed to get the string to be printed. Notice that we used the string representation of a tuple created from the array._

__str__ is not the only the method the Python data model uses to print objects; __repr__ _is also used to provide a representation of the object more oriented for debugging purposes. This representation can be evaluated to create the same object with the eval function.

For further details about the differences and the use cases of  __str__  and _ __repr__ you can refer to the Python: __str_( ) vs. repr( ) article.

from array import array
import reprlib

class Vector:

    ....

    def __repr__(self):
        s = reprlib.repr(self.__coordinates)
        return "{}({})".format(self.__class__.__name__, s[s.index('['):-1])

    ...

if __name__ == "__main__":

    v = Vector([1, 2, 3])
    s = repr(v)
    print(s)
    #Vector([1.0, 2.0, 3.0])
    v1 = eval(s)
    print(v1)
    #(1.0, 2.0, 3.0)
    v2 = Vector(range(100))
    print(repr(v2))
    #Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

In the previous code block, notice that the returned value of reprwhen used with evalpermits to create a new Vector. The use of reprliballows us not to print all the elements of the array in case it contains too many elements and replace them with.... like the vector v2.  

A Pythonic Iteration

To ensure that we can loop on our vectors and that we can unpack them, we need to make them iterables. To do so, the __iter__ method must be added to our class.

class Vector:

    ...

    def __iter__(self):
        return iter(self.__coordinates)
...

if __name__ == "__main__":

    v = Vector([1, 2, 3])
    for i in v:
        print(i)
        #1.0
        #2.0
        #3.0
    t = tuple(v)
    print(t)
    #(1.0, 2.0, 3.0)

A Pythonic Length Calculation

In order to have the capability to get the number of coordinates within our vector by assigning our objects to the len() function, the _ __len__ _method must be added to our class.

from array import array
import reprlib

class Vector:

    ...

    def __len__(self):
        return len(self.__coordinates)

    ...

if __name__ == "__main__":

    v = Vector([1, 2, 3, 4])
    print(len(v))
    #4

A Pythonic Comparaison

Without adapting our class to support comparison, the_ == _operator applied to two vector objects compare their references. To alter this behaviour, the __eq__ method must be implemented. For our example, two vectors are equal if and only if they have the same coordinates and with the same order.

from array import array
import reprlib

class Vector:

...

    def __len__(self):
        return len(self.__coordinates)

    def __iter__(self):
        return iter(self.__coordinates)

    def __eq__(self, other):
        if len(self) == len(other):
            for i, j in zip(self, other):
                if i != j:
                    return False
            return True
        else:
            return False
...

if __name__ == "__main__":

    v = Vector([0, 1, 2, 3, 4])
    v1 = Vector((1, 2, 3, 4, 5))
    v2 = Vector(range(5))
    print(v == v2)
    #True
    print(v == v1)
    #False

Let us take some time to analyze the new__eq__ method:

  • It uses the __len__ method by calling the len()function.

  • It uses the  __iter__ method by passing self and other parameters to the zip function, which accepts an iterable as parameters.

Pythonic Absolute value

For this example, we use the  __abs__ method to return the Euclidean norm of a vector defined by the below expression: 

\|\mathbf {x} \|:={\sqrt {x_{1}^{2}+x_{2}^{2}+\cdots +x_{n}^{2}}}.

_Euclidean Distance_
from array import array
from math import sqrt
import reprlib

class Vector:

   ...

    def __abs__(self):
        return sqrt(sum((x**2 for x in self)))

   ...

if __name__ == "__main__":

    v = Vector([0, 1, 2, 3, 4])
    a = abs(v)
    print(a)
    #5.477225575051661

Pythonic Boolean Evaluation

With our current implementation of the Vector class, we have the below behaviour when we evaluate the boolean value of our vectors.

if __name__ == "__main__":

    v = Vector([])
    print(bool(v))
    #False
    v1 = Vector([1, 2, 3])
    print(bool(v1))
    #True

Without the __bool__method in our class, the call to the bool() function refers to the  __len__ method. If the length is equal to 0, then the object evaluates to false — otherwise, it evaluates to true.

Let us change this behaviour by implementing a method to have it return True if the vector Euclidean norm is different from 0 and false otherwise.

from array import array
from math import sqrt
import reprlib

class Vector:

    ...

    def __abs__(self):
        return sqrt(sum((x**2 for x in self)))

    def __bool__(self):
        return bool(abs(self))
...

if __name__ == "__main__":

    v = Vector([])
    print(bool(v))
    #False
    v1 = Vector([1, 2, 3])
    print(bool(v1))
    #True
    v2 = Vector([0, 0])
    print(bool(v2))
    # False

Pythonic Slicing

The slicing in Python aims to get a subset from an initial set by indicating the index of an element to retrieve it or by indicating a slice.

A slice of an object returns another object of the same type. The method __getitem__ is the one to be updated to give our vector objects this ability.

from array import array
from math import sqrt
import reprlib

class Vector:

    ...

    def __getitem__(self, item):
        if isinstance(item, int):
            return self.__coordinates[item]
        elif isinstance(item, slice):
            return self.__class__(self.__coordinates[item])
        else:
            raise IndexError("{} indexes must be integers".format(type(self).__name__))

...

if __name__ == "__main__":

    v = Vector([1, 2, 3, 4])
    v1 = v[1]
    print(v1)
    # 2.0
    v2 = v[0:3]
    print(type(v2))
    # <class '__main__.Vector'>
    print(v2)
    # (1.0, 2.0, 3.0)

Note that by using a slice, the returned object is also a Vector object.

Conclusion

The Dunder methods that we implemented in this tutorial are not the only ones that can be used. Others, like  __bytes__,  __hash__,  __getatrr__, and ___format__  can be used to alter the behaviour of the user defined objects. These methods are not  all to be implemented every time you define a new class, it depends on your needs and this is the beauty of the Python data model. 

Thank for reading !

Python webdev object

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

How to Find Ulimit For user on Linux

Explains how to find ulimit values of currently running process or given user account under Linux using the 'ulimit -a' builtin command.

MEAN Stack Tutorial MongoDB ExpressJS AngularJS NodeJS

MEAN Stack Tutorial MongoDB ExpressJS AngularJS NodeJS - We are going to build a full stack Todo App using the MEAN (MongoDB, ExpressJS, AngularJS and NodeJS). This is the last part of three-post series tutorial.

How to configure AWS SES with Postfix MTA

Amazon Simple Email Service (SES) is a hosted email service for you to send and receive email using your email addresses and domains. Typically SES used for sending bulk email or routing emails without hosting MTA. We can use Perl/Python/PHP APIs to send an email via SES. Another option is to configure Linux or Unix box running Postfix to route all outgoing emails via SES. Before getting started with Amazon SES and Postfix, you need to sign up for AWS, including SES. You need to verify your email address and other settings. Make sure you create a user for SES access and download credentials too.

Creating RESTful APIs with NodeJS and MongoDB Tutorial

Creating RESTful APIs with NodeJS and MongoDB Tutorial - Welcome to this tutorial about RESTful API using Node.js (Express.js) and MongoDB (mongoose)! We are going to learn how to install and use each component individually and then proceed to create a RESTful API.

systemctl List All Failed Units/Services on Linux

Explains how to use the systemctl command to list all failed units or services on Debian, Ubuntu, CentOS, Arch, Fedora, and other Linux distros.