Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pythonic way to have a function return value in proper units

Objective: return a value from a function in the units (or any trivial modification) requested by the caller.

Background:

I am running Python 2.7 on a Raspberry Pi 3, and use the function distance() to get the distance a rotary encoder has turned. I need this distance in different units depending on where the function is called. How then, should this be written pythonically (i.e. short, and easily maintained).

First Attempt:

My first attempt was to use a unit of meters in the function, and have a long elif tree to select the right units to return in.

def distance(units='m'):
    my_distance = read_encoder()

    if units == 'm':
        return my_distance * 1.000
    elif units == 'km':
        return my_distance / 1000.
    elif units == 'cm':
        return my_distance * 10.00
    elif units == 'yd':
        return my_distance * 1.094
    else:
        return -1

The nice thing about this approach is that it has a way to recognize a unit that isn't available.

Second Attempt:

My second attempt was to create a dictionary to contain various multipliers.

def distance(units='m'):
    multiplier = {
        'm': 1.000,
        'km': 0.001,
        'cm': 10.00
        'yd': 1.094
    }

    try:
        return read_encoder() * mulitplier[units]
    except KeyError:
        return -1

Here, unrecognized units are caught with a KeyError.

Relevance:

I know of existing libraries like Pint, but am looking for a solution to this programming problem. When you have a function in Python, and you need to make slight modifications to the output in a reusable way. I have other functions such as speed() that use 'm/s' as a base unit, and need a similar units argument. From my experience, a well-structured program does not involve a paragraph of elif branches before every return statement. In this case, if I wanted to change how I calculate the units, I would have to carefully grep through my code, and make sure I change how the units are calculated at every instance. A proper solution would only require changing the calculation once.

This is possibly too broad, but it is a pattern I keep running into.

like image 586
Michael Molter Avatar asked Jun 29 '16 18:06

Michael Molter


People also ask

How would you return a value from a function?

To return a value from a function, you must include a return statement, followed by the value to be returned, before the function's end statement. If you do not include a return statement or if you do not specify a value after the keyword return, the value returned by the function is unpredictable.

How are functions with return value define in Python?

A return statement is used to end the execution of the function call and “returns” the result (value of the expression following the return keyword) to the caller. The statements after the return statements are not executed. If the return statement is without any expression, then the special value None is returned.

Do Python function return values have to be stored in variables?

Python functions can return multiple variables. These variables can be stored in variables directly. A function is not required to return a variable, it can return zero, one, two or more variables. This is a unique property of Python, other programming languages such as C++ or Java do not support this by default.


2 Answers

The dictionary lookup is good, but don't return a sentinel value to signal an error; just raise an appropriate exception. It could be as simple (though opaque) as letting the KeyError in your lookup propagate. A better solution, though, is to raise a custom exception:

class UnknownUnitError(ValueError):
    pass

def distance(unit='m'):
    multiplier = {
        'm': 1.000,
        'km': 0.001,
        'cm': 10.00
    }

    try:
        unit = multiplier[unit]
    except KeyError:
        # Include the problematic unit in the exception
        raise UnknownUnitError(unit)

    return read_encoder() * unit
like image 44
chepner Avatar answered Sep 20 '22 13:09

chepner


How about, using a decorator:

def read_encoder():
    return 10

multiplier = {
    'm': 1.000,
    'km': 0.001,
    'cm': 10.00,
    'yd': 1.094,
}

def multiply(fn):
    def deco(units):
        return multiplier.get(units, -1) * fn(units)
    return deco

@multiply
def distance(units='m'):  
    my_distance = read_encoder()
    return my_distance

print distance("m")
print distance("yd")
print distance("feet")

output:

10.0
10.94
-10

or, as a more generic wrapper that goes around any unit-less function:

multiplier = {
    'm': 1.000,
    'km': 0.001,
    'cm': 10.00,
    'yd': 1.094,
}

def multiply(fn):
    def deco(units, *args, **kwds):
        return multiplier.get(units, -1) * fn(*args, **kwds)
    return deco


@multiply
def read_encoder(var):
    #I've added a variable to the function just to show that
    #it can be passed through from the decorator
    return 10 * var

print read_encoder("m", 1)
print read_encoder("yd", 2)
print read_encoder("feet", 3)

output:

 10.0
 21.88
 -30

The bit about raise a KeyError vs -1 is a question of taste. Personally, I'd return * 1 if not found (if the receiver didn't care). Or throw a KeyError. The -1 isn't obviously useful.

Last iteration, making the unit parameter optional:

def multiply(fn):
    def deco(*args, **kwds):
        #pick up the unit, if given
        #but don't pass it on to read_encoder
        units = kwds.pop("units", "m")

        return multiplier.get(units, -1) * fn(*args, **kwds)
    return deco


@multiply
def read_encoder(var):
    return 10 * var

print read_encoder(1, units="yd")
print read_encoder(2)
print read_encoder(3, units="feet")


10.94
20.0  
-30
like image 190
JL Peyret Avatar answered Sep 21 '22 13:09

JL Peyret