Examples#

Let’s rewrite some of our C++ codes in python to see how the language works.

Counting and more#

Python lists have a count method. Let’s implement our Counting repetitions example.

Given a list a, we can just use a.count():

>>> a = [1, 1, 4, 6, 10, 1, 3, -2, -8, 0]
>>> a.count(1)
3

What about doing a count_if? That’s a little more tricky, but we can do a comprehension:

>>> num = len([e for e in a if e < 0])
>>> num
2

enumerate#

The rough equivalent of std::ranges::find is the index method.

If we want to find the indices for multiple matches, we can use enumerate to loop over a list and get both the index and list element at the same time.

Using the same list as above:

>>> a = [1, 1, 4, 6, 10, 1, 3, -2, -8, 0]
>>> indices = []
>>> for n, e in enumerate(a):
...     if e == 1:
...         indices.append(n)
...
>>> indices
[0, 1, 5]

This can be done as a comprehension as well:

>>> indices = [n for n, e in enumerate(a) if e == 1]
>>> indices
[0, 1, 5]

Tip

C++23 has an enumerate function available in the standard library.

Root-finding#

Let’s rewrite our Bisection root-finding method in python. This is almost a one-to-one translation. The main thing we change is that root is now passed back as the return value, and it is set to None if the root could not be found. We can do this in python because we don’t need to specify the types. In C++ this is harder.

Listing 143 bisection.py#
ATOL = 1.e-14
RTOL = 1.e-14
MAX_ITER = 100

# Apply bisection to find a root of f(x) in the range [xleft, xright]
# Return value is None if there was an error

def bisection(xleft, xright, f):

    fleft = f(xleft)
    fright = f(xright)

    if fleft * fright > 0:
        print("no root in interval [xleft, right]")
        return None

    if fleft == 0 or fright == 0:
        print("root is one of bounds")
        return None

    xmid = 0.5 * (xleft + xright)

    iter = 0

    while iter < MAX_ITER:
        fmid = f(xmid)

        if fleft * fmid > 0.0:
            # root is in [xmid, xright]
            xleft = xmid
            fleft = fmid
        else:
            # root is in [xleft, xmid]
            xright = xmid

        err = abs(xright - xleft)
        xmid = 0.5 * (xleft + xright)

        # check if we've converged
        if err < RTOL * abs(xmid) + ATOL:
            return xmid

        iter += 1

    # check to see if we took too many iterations
    print("too many iterations")
    return None


def f(x):
    return x**3 + x**2 - 1


def main():

    # bisection on f(x) = x^3 + x^2 - 1

    print("trying bisection on f(x) = x^3 + x^2 - 1")

    root = bisection(-5.0, 5.0, f)

    if root is not None:
        print(f"x_0 = {root}; f(x_0) = {f(root)}\n")
    else:
        print("root not found")

main()

Note that we end with a call to main(). This is the part that will be executed if we run this as:

python bisection

We can make it such that we can either run this on the command line (as above) or import the module (i.e., import bisection) by changing this to:

if __name__ == "__main__":
    main()

Here, __name__ is a built-in that will only be equal to __main__ if we are running from the commandline.

Extended solar system class#

We want to implement the functionality from Extending our Solar System Class. In particular, we want our Planet class to have methods for adding and getting a planet, and computing the period.

Listing 144 solar_system.py#
import math

class Planet:
    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}"


class SolarSystem:

    def __init__(self, mass):
        assert mass > 0
        self.star_mass = mass
        self.planets = []

    def add_planet(self, name, a, e=0.0):
        if any(p.name == name for p in self.planets):
            raise ValueError("planet already exists")

        self.planets.append(Planet(name, a, e))

    def print_planets(self):
        for p in self.planets:
            print(p)

    def get_planet(self, name):
        p = [p for p in self.planets if p.name == name]
        if len(p) > 0:
            return p[0]
        print(f"planet {name} does not exist")

        return None

    def get_period(self, name):
        p = self.get_planet(name)
        if p:
            return math.sqrt(p.a**3 / self.star_mass)
        return None

if __name__ == "__main__":

    ss = SolarSystem(2.0)

    ss.add_planet("alpha", 1.0)
    ss.add_planet("beta", 1.5, 0.1)
    ss.add_planet("gamma", 3.0, 0.24)

    ss.print_planets()
    print(f"period of alpha = {ss.get_period('alpha')}")

    # note that get_planet returns a reference to the Planet in our
    # solar system, so if we update it, we the change is reflected

    print()
    print("changing eccentricity of beta")

    p = ss.get_planet("beta")
    p.e = 0.9

    ss.print_planets()

    # and what happens if we try to get a planet that doesn't exist?

    print()
    print("accessing a planet that doesn't exist")

    p2 = ss.get_planet("earth")
    print(p2)

Some comments:

  • in add_planet, we do:

    any(p.name == name for p in self.planets)
    

    this if equivalent to:

    found = False
    for p in self.planets:
        if p.name == name:
            found = True
            break
    

    this is an example of comprehension.

  • in get_planet we get our Planet as:

    p = [p for p in self.planets if p.name == name]
    

    this creates a list with the entries in self.planets that have the name name. If there are no such Planet objects, then the list will be empty (so we change len(p).

    Also note that p here is the same object as in self.planets, we can think about it as a reference.