Classes#

Python doesn’t make a distinction between a struct and a class—we always use the class keyword. For example, we can create our struct Planet from Creating a struct as:

Listing 140 planet.py#
class Planet:
    """Create a Planet

    Parameters
    ----------
    name: str
        The name of the planet
    a : float
        The semi-major axis
    e : float
        The eccentricity

    """

    def __init__(self, name, a=1, e=0):
        self.name = name
        self.a = a
        self.e = e

    def __str__(self):
        return f"{self.name:10} : {self.a:5.3f}, {self.e:5.3f}"


planets = [Planet("Mercury", a=0.3871, e=0.2056),
           Planet("Venus", a=0.7233, e=0.0068),
           Planet("Earth", a=1.0000, e=0.0167),
           Planet("Mars", a=1.5237, e=0.0934),
           Planet("Jupiter", a=5.2029, e=0.0484),
           Planet("Saturn", a=9.5370, e=0.0539),
           Planet("Uranus", a=19.189, e=0.0473),
           Planet("Neptune", a=30.070, e=0.0086)]

for p in planets:
    print(p)

Some notes:

  • In python, the constructor (__init__) returns an object, so we create a Planet like:

    p = Planet("Earth")
    
  • The __init__ function is the constructor in python. Unlike C++, in python, there is only a single constructor.

  • All member functions (methods) of the class take a first argument traditionally called self. This is the python equivalent of the C++ this pointer. self refers to the object that we are working on (an instance of the class).

    When we access data stored in the class, we do it through self, e.g., self.name.

  • The __str__ method is python’s version of overloading << for output. This is the function that is called when we print() a Planet. It returns a string that we would like print to display.

  • Our planets list contains Planet, this is similar to C++ std::vector<Planet> planets.

Operator overloading#

We can overload +, -, *, /, … in python just like in C++. This is done by creating functions like __add__(self, x)__ for addition, etc. There are many of these special method names that we can implement (see in particular the emulating numeric types section).

Mathematical vector example#

Let’s implement a python version of our C++ Example: Mathematical Vectors.

Tip

We can put the class definition in its own .py file, and as long as that is in the same directory (or installed in a systemwide search path), we can import it.

This works similar to creating a header file in C++.

Here’s a python module that implements our vector class:

Listing 141 vector.py#
import math


class Vector:
    """ a general two-dimensional vector """

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"({self.x} î + {self.y} ĵ)"

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        # it doesn't make sense to add anything but two vectors
        raise NotImplementedError(f"adding {type(other)} to Vector not defined")

    def __sub__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x - other.x, self.y - other.y)
        # it doesn't make sense to add anything but two vectors
        raise NotImplementedError(f"subtracting {type(other)} from Vector not defined")

    def __mul__(self, other):
        if isinstance(other, (int, float)):
            # scalar multiplication changes the magnitude
            return Vector(other*self.x, other*self.y)
        raise NotImplementedError("multiplication not defined")

    def __matmul__(self, other):
        # a dot product
        if isinstance(other, Vector):
            return self.x*other.x + self.y*other.y
        raise NotImplementedError("matrix multiplication not defined")

    def __rmul__(self, other):
        return self.__mul__(other)

    def __truediv__(self, other):
        # we only know how to multiply by a scalar
        if isinstance(other, (int, float)):
            return Vector(self.x/other, self.y/other)
        raise NotImplementedError("we don't know how to divide two Vectors")

    def __abs__(self):
        return math.sqrt(self.x**2 + self.y**2)

    def __neg__(self):
        return Vector(-self.x, -self.y)

    def cross(self, other):
        """a vector cross product -- we return the magnitude, since it will
        be in the z-direction, but we are only 2-d"""
        return abs(self.x*other.y - self.y*other.x)

Some comments:

  • We use isinstance to check if the type of other makes sense. If not, we raise an exception that halts the execution.

  • In addition to __str__, we also include __repr__ which is the representation of our object. That is used when the Vector is displayed via means other than print see this stackoverflow discussion on __repr__ vs __str__.

  • The / operator is called __truediv__ in python.

  • We have a __matmul__ operator, which is invoked by the @ operator. This is intended to be used for dot-products and matrix multiplication.

  • Just like in C++, we treat vec * a and a * vec differently (where vec is a Vector), since we cannot assume the objects commute under multiplication. We use __rmul__ to express right-multiplication.

and here’s an example of using it—note the import vector at the top.

Listing 142 test_vector.py#
import vector

u = vector.Vector(3, 5)
v = vector.Vector(1, 2)

print(f"u = {u}")
print(f"v = {u}")
print()

# addition and subtraction
print(f"{u} + {v} = {u + v}")
print(f"{u} - {v} = {u - v}")
print()

# multiplication and division
print(f"2 * {u} = {2 * u}")
print(f"{u} * 2 = {u * 2}")
print(f"{u} / 3 = {u / 3}")

# abs, dot, and cross
print(f"abs(u) = {abs(u)}")
print(f"u . v = {u @ v}")
print(f"u x v = {u.cross(v)}")

Important

When we do import vector we are importing the module named vector. We need to use that module name as the namespace when accessing the contents of the module, so we do vector.Vector() to create an instance of our Vector class.