Pythonic attempt at conversion of length 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.

Note: The code works on both Python 3 and 2.

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
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) -&gt; Number:
    if value % 1:
        return float(value)
    else:
        return int(value)


def convert(value: Number, unit_in: str, unit_out: str) -&gt; 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'))