7
\$\begingroup\$

I'm trying to learn Python, so I started with a program to convert length units, mixing imperial and metric. First I wrote a bunch of small simple functions, but then I wanted to solve the issue of having an intuitive way of calling one function with some easy to remember parameter values in order to convert from any unit to any other unit, as opposed to having to call a bunch of different function names.

Does each instance of Convert remain in memory when I am using __new__ and not actually returning an instance of the class? For example, since only a float is stored in 'x' at the end, is the instance of convert that was created destroyed?

Is this approach valid overall? It feels like I've done something a little strange here.

class Convert(object):
    def __new__(self, val, unit_in, unit_out):
        convertString   = '%s_to_%s' % (unit_in, unit_out)
        try:
            convertFunction = getattr(self, convertString)
        except:
            return None
        return convertFunction(self, val)

    def mm_to_cm(self, millimeters):
        return millimeters / 10.0
    def mm_to_in(self, millimeters):
        return ((millimeters / 1000.0) / 0.9144) * 36.0
    def mm_to_ft(self, millimeters):
        return ((millimeters / 1000.0) / 0.9144) * 3.0
    def mm_to_m(self, millimeters):
        return millimeters / 1000.0

    def cm_to_mm(self, centimeters):
        return centimeters * 10.0
    def cm_to_in(self, centimeters):
        return ((centimeters / 100.0) / 0.9144) * 36.0
    def cm_to_ft(self, centimeters):
        return ((centimeters / 100.0) / 0.9144) * 3.0
    def cm_to_m(self, centimeters):
        return centimeters / 100.0

    def in_to_mm(self, inches):
        return (((inches / 12.0) / 3.0) / (1.0 / 0.9144)) * 1000.0
    def in_to_cm(self, inches):
        return (((inches / 12.0) / 3.0) / (1.0 / 0.9144)) * 100.0
    def in_to_ft(self, inches):
        return inches / 12.0
    def in_to_m(self, inches):
        return (((inches / 12.0) / 3.0) / (1.0 / 0.9144))

    def ft_to_mm(self, feet):
        return (feet / 3.0) / (1.0 / 0.9144) * 1000.0
    def ft_to_cm(self, feet):
        return (feet / 3.0) / (1.0 / 0.9144) * 100.0
    def ft_to_in(self, feet):
        return feet * 12.0
    def ft_to_m(self, feet):
        return (feet / 3.0) / (1.0 / 0.9144)

    def m_to_mm(self, meters):
        return meters * 1000.0
    def m_to_cm(self, meters):
        return meters * 100.0
    def m_to_in(self, meters):
        return (meters / 0.9144) * 36.0
    def m_to_ft(self, meters):
        return (meters / 0.9144) * 3.0

print(Convert( 1.0,   'ft', 'in'))
print(Convert(12.0,   'in', 'ft'))
print(Convert( 0.3048, 'm', 'ft'))
x = Convert(3, 'ft', 'm')
print(x)
exit()
output:
12.0
1.0
1.0
0.9144
\$\endgroup\$
8
\$\begingroup\$

Firstly as highlighted by Tlomoloko using a class for this is really strange.

However I think the both of your are missing something rather important. If you wanted to hand write every SI prefix for metres alone then you'll rack up an unmaintainable amount of code. The table contains 21 prefixes, such as no prefix - m, centi - cm, and kilo - km. Whilst I'm sure many conversions you'll think are ridiculous. Who needs to convert from yoctometres to yottametres? But what's the harm in supporting it, if you allow something reasonable like zetta to yotta?

To write the conversions for all SI prefixes would require only \$\binom{21+1}{2}\$ or 231 bespoke functions. Which would be absolutely ridiculous to write by hand.

And so the solution to this is to have an intermarry value that you always convert to or from. And since all of your existing functions are nice simple multiplications or divisions we can simply assign a single value for each unit.

This may be a bit hard to visualize, and so we'll run through some examples.

Example

  • 1 cm to mm

    1 cm = 0.01 m
    1 mm = 0.001 m

    First we convert 1 cm to metres. This is as simple as \$1 \times 0.01\$. Afterwards we convert from metres to millimetres simply as \$\frac{1 \times 0.01}{0.001}\$.

    Which results in 10.

  • 1' to inches

    1' = 0.3048 m
    1" = 0.0254 m

    First we convert 1' to meters. This is as simple as \$1 \times 0.3048\$. Afterwards we convert from metres to foot simply as \$\frac{1 \times 0.3048}{0.0254}\$.

    Which results in 12.

Code

CONVERSIONS = {
    'm': 1,
    'cm': 0.01,
    'mm': 0.001,
    'in': 0.0254,
    'ft': 0.3048,
}

def convert(value, unit_in, unit_out):
    return value * CONVERSIONS[unit_in] / CONVERSIONS[unit_out]


print(convert( 1.0,   'ft', 'in'))
print(convert(12.0,   'in', 'ft'))
print(convert(0.3048, 'm',  'ft'))
print(convert(3,      'ft', 'm' ))

Now the code's not perfect. If you run it you should instantly notice that it outputs some ugly 12.000000000000002 rather than 12.0. Yuck.

And so we can convert your code to use fractions.Fraction. However this will print 1143/1250 rather than 0.9144. Since I dislike getting 12.0 rather than 12, we can fix these at the same time.

from typing import Dict, Union
from fractions import Fraction

Number = Union[int, float]
CONVERSIONS: Dict[str, Fraction] = {
    'm': Fraction('1'),
    'cm': Fraction('0.01'),
    'mm': Fraction('0.001'),
    'in': Fraction('0.0254'),
    'ft': Fraction('0.3048'),
}


def to_number(value: Fraction) -> Number:
    if value % 1:
        return float(value)
    else:
        return int(value)


def convert(value: Number, unit_in: str, unit_out: str) -> Number:
    return to_number(value * (CONVERSIONS[unit_in] / CONVERSIONS[unit_out]))


if __name__ == '__main__':
    print(convert( 1.0,    'ft', 'in'))
    print(convert(12.0,    'in', 'ft'))
    print(convert( 0.3048,  'm', 'ft'))
    print(convert( 3,      'ft',  'm'))
\$\endgroup\$
  • \$\begingroup\$ I was using a class as sort of a stand-in for a struct. My posted code did not entirely reflect my intent as I was still studying the notion of "data classes" docs.python.org/3/library/dataclasses.html I appreciate your criticisms and approach and will study the fractions module. \$\endgroup\$ – AlanK Feb 9 at 0:04
  • \$\begingroup\$ @AlanK I don't think using any of a class, a dataclass, or a named tuple would be beneficial here. Please understand that any deception in a question can lead to drastically different answers. In the future please post your finished code verbatim for the best experience. \$\endgroup\$ – Peilonrayz Feb 9 at 20:35
  • \$\begingroup\$ Deception? I think you should choose another word. I said my code did not entirely reflect my intent because I was still studying data classes. To put it another way, I didn't understand data classes yet, so my example code did not reflect the correct implementation of one. That is not "deception". Deception suggests an intent to mislead, and I certainly had no such intent. I don't know if English is your first language or not, but in common parlance, to call someone "deceptive" is about the same as calling them a liar. Harsh. \$\endgroup\$ – AlanK Feb 11 at 9:08
  • \$\begingroup\$ @AlanK At no point did I call you deceptive or a liar. \$\endgroup\$ – Peilonrayz Feb 11 at 9:44
6
\$\begingroup\$

I think that it's a bit weird to use classes as a way to return values, I don't know if that's common in python, but anyways. The problem with your code, in my opinion, is that if you are converting a unit to another, ex: cm to in, you don't need to create a class that will store information about other units, like: inches to m. That ends up happening in your code whenever you do print(Convert(val, unit_in, unit_out)). Here's my solution:

class LengthUnit():
    # This class stores the unit's ratio to all the others
    def __init__(self, to_mm, to_cm, to_m, to_in, to_ft):
        self.to_mm = to_mm
        self.to_cm = to_cm
        self.to_m = to_m
        self.to_in = to_in
        self.to_ft = to_ft

# This function returns the conversion of the value
# The value is divided by the first number in the ratio,
# and multiplied by the second number (it's a tuple).
def convert_unit(val, unit_in, unit_out):
    unit_out = 'to_{}'.format(unit_out)
    try:
        return val / getattr(unit_in, unit_out)[0] * getattr(unit_in, unit_out)[1]
    except AttributeError:
        return 'Invalid Units'

if __name__ == '__main__':
    # In this dictionary, you store all the unit classes
    length_units = {'mm' : LengthUnit(to_mm= (1.0, 1.0), to_cm= (0.1, 1.0), to_m= (0.001, 1.0), to_in= (25.4, 1), to_ft= (305, 1)),
                    'cm' : LengthUnit(to_mm= (1.0, 10), to_cm= (1.0, 1.0), to_m= (100.0, 1.0), to_in= (2.54, 1), to_ft=(30.48, 1.0)),
                    'm' : LengthUnit(to_mm= (1.0, 1000), to_cm= (1.0, 100.0), to_m= (1.0, 1.0), to_in= (1.0, 39.37), to_ft=(1.0, 3.281)),
                    'in' : LengthUnit(to_mm= (1.0, 25.4), to_cm= (1.0, 2.54), to_m= (39.37, 1.0), to_in=(1.0, 1.0), to_ft= (12.0, 1)),
                    'ft' : LengthUnit(to_mm= (1.0, 305), to_cm= (1.0, 30.48), to_m= (3.281, 1.0), to_in= (1, 12.0), to_ft= (1.0, 1.0))
                    }

    print(convert_unit(5, length_units['cm'], 'm'))
    print(convert_unit(10, length_units['ft'], 'mm'))
    print(convert_unit(23, length_units['in'], 'm'))
    # If a non-defined unit is parsed, a error message is printed.
    print(convert_unit(5, length_units['mm'], 'miles'))

This is all a rough sketch, you can adjust the values if you want them to be more exact, or change the function if you want it to print the values instead of returning them. Hope it was helpful!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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