17. Classes, Objects, Attributes, Methods: Very Basic Object-Oriented Programming in Python

This work is licensed under Creative Commons Attribution-ShareAlike 4.0 International


This is a very basic introduction to object-oriented prgramming in Python: defining classes and using them to create objects with methods (a cousin of functions) that act on those objects.

These are illustrated with objects that are vectors, and in particular 3-component vectors that have a cross product.

The first class will be for vectors wih three components, labeld ‘x’, ‘y’ and ‘z’.

On its own, this would be rather redundant, since numpy arrays could be used, but it serves to introduce some basic ideas, and then prepare for the real goal: 3-vectors with cross product.

17.1. Example A: Class VeryBasic3Vector

This first class VeryBasic3Vector illustrates some basic features of creating and using classes; however, it will be superceded soon!

Almost every class has a method with special name __init__, which is use to create objects of this class. In this case, __init__ sets the three attributes on a VeryBasic3Vector — its x, y, and z components.

This class has just one other method, for the scalar (“dot”) product of two such vectors.

class VeryBasic3Vector():
    """A first, minimal class for 3-component vectors, offering just creation and the scalar ("dot") product."""
    def __init__(self, values):
        self.x = values[0]
        self.y = values[1]
        self.z = values[2]
    def dot(self, other):
        '''"a.dot(b)" gives the scalar ("dot") product of a with b'''
        return self.x * other.x + self.y * other.y + self.z * other.z

Create a couple of VeryBasic3Vector objects:

a = VeryBasic3Vector([1, 2, 3])
print(f"The x, y, and z attributes of object a are {a.x}, {a.y} and {a.z}")
b = VeryBasic3Vector([4, 5, 2])
print(f"Object b contains the vector <{b.x}, {b.y}, {b.z}>")

This way of printing values one attribute at a time gets tiresome, so soon, an alternative will be introduced, in improved class BasicNVector

The attributes of an object can also be set directly:

a.y = 5
print(f"a is now <{a.x}, {a.y}, {a.z}>")

Methods are used as follows, with the variable before the “.” part being self and any parenthesized variables being the arguments to the method definition.

print(f'The scalar ("dot") product of a and b is {a.dot(b)}')

17.2. Example B: Class BasicNVector

A second class BasicNVector with some improvements over the above class VeryBasic3Vector:

  • Allowinv vecors of any length, “N”

  • Methods for addition, subtraction and vector-by-scalar multiplication.

  • The special method __str__ (using this name is mandatory) to output an object’s values as a string — for using in print, for example.

Aside: here and below, the names of these special methods like __str__ start and end with a pair of underscores, “__”.

class BasicNVector():
    """This improves on class VeryBasic3Vector by:
    - allowing any number of components,
    - adding several methods for vector arithmetic, and
    - defining the special method  __str__() to help display the vector's value."""

    # First mimic the definitions in class VeryBasic3Vector:
    
    def __init__(self, list_of_components):
        self.list = list_of_components.copy()

    def dot(self, other):
        '''"a.dot(b)" gives the scalar ("dot") product of a with b'''
        dot_product = 0
        for i in len(self):
            dot_product += self.list[i] * other.list[i]
        return dot_product

    # Next, some new stuff not in class VeryBasic3Vector

    def times(self, scale):
        '''"self.times(scale)"" gives the product of vector "self" by scalar "scale"'''
        return BasicNVector([scale * component for component in self.list])  # This uses a list comprehension; noteson comprehensions are coming soon!

    # The special method names wrapped in double underscores, __add__, __sub__ and __str__,
    # have special meanings, as will be revealed below.

    def __add__(self, other):  # Vector addition — with definition of the "+" operation for pairs of BasicNVector objects
        return BasicNVector([ self.list[i] + other.list[i] for i in range(len(self.list)) ])

    def __sub__(self, other):  # Vector subtraction — with definition of the "-" operation for pairs of BasicNVector objects
        return BasicNVector([ self.list[i] - other.list[i] for i in range(len(self.list)) ])

    def __str__(self):
        """How to convert the value to a text string.
        As above, this uses angle brackets <...> for vectors, to distinguish from lists [...] and tuples (...)"""
        string = '<'
        for component in self.list[:-1]:
            string += f'{component}, '
        string += f"{self.list[-1]}>"
        return string

We need to create new objects of class BasicNVector to use these new methods:

c = BasicNVector([1, 2, 3, 4])
d = BasicNVector([4, 5, 2, 3])

The new method “__str__” makes it easer to display the value of a BasicNVector, by just using print:

print("c = ", c)

And now we can try the other new methods:

c_times_3 = c.times(3)
print(f"{c} times 3 is {c_times_3}")
<1, 2, 3, 4> times 3 is <3, 6, 9, 12>

Here is the usual way of using the mysteriously-names method “__add__” …

e = c.__add__(d)
print(f"{c} + {d} = {e}")
<1, 2, 3, 4> + <4, 5, 2, 3> = <5, 7, 5, 7>

… but that special name also means that it also specifies how the operation “+” works on a pair of BasicNVector objects:

f = c + d
print(f'{c} + {d} = {f}')
<1, 2, 3, 4> + <4, 5, 2, 3> = <5, 7, 5, 7>

Likewise for subtraction with __sub__:

print(f'{c} - {d} = {c - d}')
<1, 2, 3, 4> - <4, 5, 2, 3> = <-3, -3, 1, 1>

17.3. Inheritence: new classes that build on old ones

A new class can be defined by refining an existing one, by adding methods and such, to avoid defining everything from scratch. The basic syntax for creating class ChildClass based on an existing parent class named ParentClass is

class ChildClass(ParentClass):

Here we define class Vector3, which is restricted to vectors with 3 components, and uses that restriction to allow defining the vector cross product.

In addition, it makes the operator “*” do cross multiplication on such objects, by also defining the special method __mul__:

class Vector3(BasicNVector):
    """Restrict to BasicNVector objects of length 3, and then add the vector cross product"""
    def __init__(self, list_of_components):
        if len(list_of_components) == 3:
            super().__init__(list_of_components)
            # Aside: function "super()" gives the parent class, so the above is equivalent to
            #BasicNVector.__init__(self, list_of_components)
        else:  # Complain!
            raise ValueError('The length of a Vector3 object must be 3.')

    def cross_product(self, other):  # the vector cross product
        (x1, y1, z1) = self.list
        (x2, y2, z2) = other.list
        return Vector3([ y1*z2 - y2*z1, z1*x2 - z2*x1, x1*y2 - x2*y1] )

    # Add a synonym: now it redefines the multiplication operator "*"
    # I could have just used the name `__mul__` intead of `cross_product` above;
    # I did it this was to also allow the more descriptive name `cross_product`, and to illustrate the use of synonyms for functions.
    __mul__ = cross_product  

Again, we need some Vector3 objects; the above BasicNVector objects do not know about the cross product.

But note that the previously-defined methods for class BasicNVector also work for Vector3 objects, so for example we can still print with the help of method __str__ from there.

u = Vector3([1, 2, 3])
v = Vector3([4, 5, 10])
print(f'The vector cross product of {u} with {v} is {u.cross_product(v)}')
print(f'The vector cross product of {v} with {u} is {v*u}')
The vector cross product of <1, 2, 3> with <4, 5, 10> is <5, 2, -3>
The vector cross product of <4, 5, 10> with <1, 2, 3> is <-5, -2, 3>

This is what happens with inappropriate input, thanks to that raise command:

w = Vector3([1, 2, 3, 4])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-16-4dc0f24dc07a> in <module>
----> 1 w = Vector3([1, 2, 3, 4])

<ipython-input-14-a85930062fea> in __init__(self, list_of_components)
      7             #BasicNVector.__init__(self, list_of_components)
      8         else:  # Complain!
----> 9             raise ValueError('The length of a Vector3 object must be 3.')
     10 
     11     def cross_product(self, other):  # the vector cross product

ValueError: The length of a Vector3 object must be 3.

Aside: That’s ugly: as seen in the notes on Exceptions and Exception Handling, a more careful usage would be:

try:
    w = Vector3([1, 2, 3, 4])
except Exception as what_just_happened:
    print(f"Well, at least we tried, but: '{what_just_happened.args[0]}'")
Well, at least we tried, but: 'The length of a Vector3 object must be 3.'