Pythonic способ получить возвращаемое значение функции в соответствующих единицах

Цель: вернуть значение из функции в единицах (или любой тривиальной модификации), запрошенной вызывающим.

Фон:

Я запускаю Python 2.7 на малине Pi 3 и использую функцию distance(), чтобы получить расстояние, на которое повернулся поворотный кодер. Мне нужно это расстояние в разных единицах в зависимости от того, где вызывается функция. Как тогда, должно ли это быть написано питонически (т.е. Коротким и легко поддерживать).

Первая попытка:

Моя первая попытка состояла в том, чтобы использовать единицу счетчиков в функции и иметь длинное дерево elif для выбора правильных единиц для возврата.

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

Хорошая вещь в этом подходе заключается в том, что у него есть способ распознать недоступный модуль.

Вторая попытка:

Моя вторая попытка состояла в том, чтобы создать словарь, содержащий различные 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

Здесь нераспознанные единицы захватываются с помощью KeyError.

Актуальность:

Я знаю существующие библиотеки, такие как Pint, но я ищу решение этой проблемы программирования. Когда у вас есть функция в Python, и вам нужно внести небольшие изменения в результат многоразовым способом. У меня есть другие функции, такие как speed(), которые используют "m/s" в качестве базового блока и нуждаются в аналогичном аргументе units. По моему опыту, хорошо структурированная программа не включает в себя абзац ветвей elif перед каждым оператором return. В этом случае, если бы я хотел изменить способ вычисления единиц, мне пришлось бы тщательно grep через мой код и убедиться, что я изменяю, как единицы вычисляются в каждом экземпляре. Правильное решение потребовало бы только однократного изменения расчета.

Это возможно слишком широко, но это шаблон, в котором я продолжаю работать.

Ответ 1

Как насчет, используя декоратор:

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")

выход:

10.0
10.94
-10

или, как более общая оболочка, которая обходит любую функцию без единицы:

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)

выход:

 10.0
 21.88
 -30

Бит о повышении KeyError против -1 - это вопрос вкуса. Лично я бы вернул * 1, если не нашел (если получателю все равно). Или бросьте KeyError. Значение -1 явно не полезно.

Последняя итерация, делая параметр единицы необязательным:

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

Ответ 2

Словарь поиска хорош, но не возвращает значение дозорного сигнала, чтобы сигнализировать об ошибке; просто поднимите соответствующее исключение. Это может быть как простой (хотя и непрозрачный), так и способ распространения KeyError в вашем поиске. Лучшее решение, однако, состоит в том, чтобы поднять пользовательское исключение:

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

Ответ 3

Для вашего примера это может быть:

class DistanceUnits():
    """
    Enum class for indicating measurement units and conversions between them.
    Holds all related logic.
    """
    M = 'm'
    KM = 'km'
    CM = 'cm'


def distance(units=DistanceUnits.M):
    multiplier = {
        DistanceUnits.M: 1.000,
        DistanceUnits.KM: 0.001,
        DistanceUnits.CM: 10.00
    }
    return read_encoder() * mulitplier[units] if units in multiplier else -1

Но может быть разумным переместить multipliers вне функции distance и сделать его частью DistanceUnits.

UPD: существует множество разных способов "как...", и все они зависят от ваших потребностей, но есть один главный принцип DRY. Даже много elif может быть достаточно хорошим (для создания словаря каждый экземпляр использует один барабан для каждого вызова функции.), Если вы не забудете, не повторяйте себя.