Calmcode - objects: final advice

Don't overdo it.

1 2 3 4 5 6 7 8 9 10

As a final addition, let's consider adding the __call__ method.

import pandas as pd
import marimo as mo
import altair as alt
import random

class Dice:
    def __init__(self, probs):
        self.probs = probs
        self.expected_value = sum(i * p for i, p in self.probs.items())

    @classmethod
    def from_sides(cls, n=6):
        return Dice({i: 1/n for i in range(1, n + 1)})

    def roll(self, n=1):
        sides = list(self.probs.keys())
        probabilities = list(self.probs.values())
        return random.choices(sides, weights=probabilities, k=n)

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

    def _operate(self, other, operator):
        if isinstance(other, (float, int)):
            other = Dice({other: 1})
        new_probs = {}
        for s1, p1 in self.probs.items():
            for s2, p2 in other.probs.items():
                new_key = operator(s1, s2)
                if new_key not in new_probs:
                    new_probs[new_key] = 0
                new_probs[new_key] += p1 * p2
        return Dice(new_probs)

    def __add__(self, other):
        return self._operate(other, lambda a,b: a + b)

    def __sub__(self, other):
        return self._operate(other, lambda a,b: a - b)

    def __mul__(self, other):
        return self._operate(other, lambda a,b: a * b)

    def __call__(self, n=1):
        return self.roll(n=n)

    def _repr_html_(self):
        return mo.as_html(self.prob_chart()).text

    def prob_chart(self):
        df = pd.DataFrame([{"i": k, "p": v} for k, v in self.probs.items()])
        return (
            alt.Chart(df)
              .mark_bar()
              .encode(x="i", y="p")
              .properties(title="Dice with probabilities:", width=120, height=120)
        )

Does this make the object better? In our opinion: not really.

We have a pretty functional .roll() method that is easy to understand. Adding a call method makes sense when there is a clear single expected behavior. And right out of the blue, it might not be clear that calling dice() runs a simulation. Simply because there are many things that a dice can do.

This brings us to the topic of "overdoing it". There are a lot of features when it comes to objects in Python and we've really only been scratching the surface. It can be very tempting to use all of them. But let's not do that. The main goal of classes and objects is that they can give us a better developer experience because they allow us to group related code together. Anything that we do that goes beyond that might come at the risk of overdoing it and making the code harder to understand.

It's a balancing act. But we do hope that you'll consider writing some objects in your code and that it might help you keep the code clean.