5
\$\begingroup\$

Function:

This is to calculate the values for the equation: $$\rho = \frac{m}{v} = \frac{mass}{volume}$$

what I need help with:

I still can’t get my head around classes and want to know if I’m doing them right any suggestions are most welcome.

Code:

from decimal import Decimal
from dataclasses import dataclass

@dataclass
class Pmv:
    den: float
    vol: float
    mass: float
    
    def get_vol(self) -> Decimal:                
        return round(
          Decimal(
              self.mass
          ) /
          Decimal(
              self.den
          ), 2
        )

    def get_den(self) -> Decimal:
        return round(
          Decimal(
              self.mass
          ) *
          Decimal(
              self.vol
          ), 2
        )

    def get_mass(self) -> Decimal:
         return round(
           Decimal(
               self.den
           ) /
           Decimal(
               self.vol
           ), 2
         )
    
vol = Pmv(6.2, 0, 2.1)
print(vol.get_vol())

den = Pmv(0, 3.1, 4.8)
print(den.get_den())

mass = Pmv(8.9, 2.2, 0)
print(mass.get_mass())

Output:

0.34
14.88
4.05

Goal:

If I have gotten this right I’m going to make a class for a lot of chemistry equations which would simplify my calculations for me.

\$\endgroup\$
1
  • \$\begingroup\$ Is what you want a class that lets you provide any two of the three quantities, calculates the third, and gives you any of the three on demand? Or something else? If so, you would actually want three factory functions that take pm, mv or pv, and return a Pmv. \$\endgroup\$
    – Davislor
    May 1, 2022 at 21:08

3 Answers 3

12
\$\begingroup\$

This is not a good application of a class. You will never have all three members to initialise the class, so they all have to be marked Optional. Your class does nothing to stop someone from making an "overdetermined" instance where all three members are set and one of them is wrong. And once you do have an instance, you'll have three different members where you only care about two, and three different methods when you only care about one.

Don't round() in the middle of calculations. Only do that upon presentation, as implied by the decimal precision field in a formatting call.

A function that fills in the gap could look like

from decimal import Decimal
from typing import Optional


def solve_density(
    density: Optional[Decimal] = None,
    volume: Optional[Decimal] = None,
    mass: Optional[Decimal] = None,
) -> Decimal:
    # density = mass / volume

    if density is None:
        return mass / volume
    if volume is None:
        return mass / density
    if mass is None:
        return density * volume


def test() -> None:
    volume = solve_density(density=Decimal('0.02'), mass=Decimal('0.1'))
    print(f'volume = {volume:.2f}')


if __name__ == '__main__':
    test()

This still has the over-determined problem. It doesn't offer any advantages over just having three separate, purpose-built functions, each accepting only two parameters.

There is a very different approach that:

  • requires installing and importing Sympy;
  • does not require that you re-express the equation for every output;
  • is a slightly better use-case for a class;
  • captures some of the physical constraints of your variables - they're non-negative, real, and finite; and
  • is overkill for some applications but convenient for others:

Define a class that holds a dictionary of symbols, and the equation in question. At solution time, call a single method passing in your knowns as kwargs and getting back your unknown as a float.

This has a certain amount of in-built validation - it will bomb out if you pass unknown variables, the wrong number of variables, or variables that produce a mathematical domain problem.

from sympy import Equality, solve, Symbol


def real(name: str) -> Symbol:
    return Symbol(name=name, real=True, finite=True, nonnegative=True)


class DensitySystem:
    def __init__(self) -> None:
        p = real('p')
        m = real('m')
        v = real('v')
        self.symbols = {s.name: s for s in (p, m, v)}
        self.equation = Equality(p, m*v)

    def solve(self, **kwargs: float) -> float:
        unknown, = self.symbols.keys() - kwargs.keys()
        solved, = solve(self.equation, self.symbols[unknown])
        value = solved.subs({
            self.symbols[k]: known
            for k, known in kwargs.items()
        })
        return float(value)


def test() -> None:
    system = DensitySystem()
    v = system.solve(p=1, m=2)
    print(f'v={v:.2f}')


if __name__ == '__main__':
    test()
\$\endgroup\$
8
  • \$\begingroup\$ Thanks a lot for your input, I’ll take on board passing Decimal(value) to the function and using the print truncate, the only problem is this is just a function rather than a class, and I’m trying to understand classes \$\endgroup\$
    – Nickotine
    Apr 30, 2022 at 14:00
  • 1
    \$\begingroup\$ Sure, so do something that warrants a class. I'll propose an edit in a few minutes. \$\endgroup\$
    – Reinderien
    Apr 30, 2022 at 14:00
  • \$\begingroup\$ Do you think I can’t really use this as a class? My first idea was to have a bunch of chemistry equations in a class but I had a lot of difficulty implementing it so just went with 1 equation thinking I’d make a seperate file for each equation \$\endgroup\$
    – Nickotine
    Apr 30, 2022 at 14:02
  • 4
    \$\begingroup\$ "Can't" and "shouldn't" are two different things. You can do anything, but you should be learning about an application that actually calls for a class, not one that shoehorns it in. I am writing an edit that proposes the use of Sympy. \$\endgroup\$
    – Reinderien
    Apr 30, 2022 at 14:03
  • 1
    \$\begingroup\$ This looks very cool I will study it and play with it thank you very much as always for your input \$\endgroup\$
    – Nickotine
    Apr 30, 2022 at 15:51
2
\$\begingroup\$

I agree that the class is not the right way to organize these calculations unless you will store the values and implement the calculations as part of a larger system. If you are not storing any values but just handling basic calculations, I propose a simpler, more direct functional style that just implements the calculations as functions ("verbs"), ex.

get_vol(mass, den) 
get_den(mass, vol)
get_mass(den, vol)

that way you can never have an over- or under-determined system if you must pass in values. If you think that is too much repetition, you can implement solve_density(mass, den, vol) that does error checking to ensure exactly one of the inputs is None.

Another idea for a more featureful system is to implement units as classes ("nouns"), ex. Kilogram(3). This way you can implement dimensional analysis checks to ensure you're always using the right units.

Small things: You also haven't given details if the Decimal class is necessary. Unless you need exact computations, you can get get away with using floats for real world calculations. The rounding sigfigs should only be for display anyhow. Also I'm not a fan of the indentation style which uses too many newlines.

\$\endgroup\$
2
  • \$\begingroup\$ That’s very interesting, how can I get dimensional analysis into this? I like @reinderien way as it automatically rearranges the equation but reading what you said about dimensional analysis makes me very curious and excited \$\endgroup\$
    – Nickotine
    May 2, 2022 at 22:43
  • \$\begingroup\$ I'm not sure how it would be implemented, it's just something I saw from sympy be able to do \$\endgroup\$
    – qwr
    May 3, 2022 at 1:18
1
\$\begingroup\$

("Data classes" is entirely the wrong end to get the knack of OO modelling, design & coding.
Look for introductions/tutorials/examples where the are classes denoting concepts and specialisations/refinements thereof as well as multiple instances of at least one of the classes,)

  • Don't write, never publish uncommented/undocumented code.
    Python got it right specifying docstrings such that it is easy to copy them with the code, and tedious to copy the code without them.
  • Naming
    You named one of the attributes mass -
    the others should be volume and density.
    With a name of DensityMassVolume as well as Pmv, declaration order should not "contradict".

A density of almost 15 with volume and mass almost equal is a follow-on error.

\$\endgroup\$
1
  • \$\begingroup\$ Also Pmv is not a clear class name and is probably better named something like DensitySystem \$\endgroup\$
    – qwr
    May 1, 2022 at 20:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.