Стиль кода if-else Python для сокращенного кода для округления чисел с плавающей точкой

Есть ли более короткий, более разборчивый стиль кода для решения этой проблемы? Я пытаюсь классифицировать некоторые значения с плавающей точкой в межрегиональные папки.

def classify(value):   
    if value < -0.85 and value >= -0.95:
        ts_folder = r'\-0.9'
    elif value < -0.75 and value >= -0.85:
        ts_folder = r'\-0.8'
    elif value < -0.65 and value >= -0.75:
        ts_folder = r'\-0.7'    
    elif value < -0.55 and value >= -0.65:
        ts_folder = r'\-0.6'   
    elif value < -0.45 and value >= -0.55:
        ts_folder = r'\-0.5'  
    elif value < -0.35 and value >= -0.45:
        ts_folder = r'\-0.4'
    elif value < -0.25 and value >= -0.35:
        ts_folder = r'\-0.3'
    elif value < -0.15 and value >= -0.25:
        ts_folder = r'\-0.2'
    elif value < -0.05 and value >= -0.15:
        ts_folder = r'\-0.1'
    elif value < 0.05 and value >= -0.05:
        ts_folder = r'\0.0'
    elif value < 0.15 and value >= 0.05:
        ts_folder = r'\0.1'
    elif value < 0.25 and value >= 0.15:
        ts_folder = r'\0.2'
    elif value < 0.35 and value >= 0.25:
        ts_folder = r'\0.3'
    elif value < 0.45 and value >= 0.35:
        ts_folder = r'\0.4'
    elif value < 0.55 and value >= 0.45:
        ts_folder = r'\0.5'
    elif value < 0.65 and value >= 0.55:
        ts_folder = r'\0.6'
    elif value < 0.75 and value >= 0.65:
        ts_folder = r'\0.7'  
    elif value < 0.85 and value >= 0.75:
        ts_folder = r'\0.8'
    elif value < 0.95 and value >= 0.85:
        ts_folder = r'\0.9'

    return ts_folder

Ответ 1

Конкретное решение

Реального общего решения не существует, но в вашем случае вы можете использовать следующее выражение.

ts_folder = r'\{:.1f}'.format(round(value, 1))

Общее решение

Если вам действительно нужно какое-то обобщение, обратите внимание, что любой нелинейный паттерн вызовет проблемы. Хотя есть способ сократить код.

def classify(key, intervals):
    for lo, hi, value in intervals:
        if lo <= key < hi:
            return value
    else:
        ... # return a default value or None

# A list of tuples (lo, hi, key) which associates any value in the lo to hi interval to key
intervals = [
    (value / 10 - 0.05, value / 10 + 0.05, r'\{:.1f}'.format(value / 10))
    for value in range(-9, 10)
]

value = -0.73

ts_folder = classify(value, intervals) # r'\-0.7'

Обратите внимание, что вышеприведенное все еще не полностью защищено от некоторой ошибки округления с плавающей точкой. Вы можете добавить точность, вручную вводя список intervals вместо использования понимания.

Непрерывные интервалы

Если интервалы в ваших данных непрерывны, то есть между ними нет разрыва, как в вашем примере, тогда мы можем использовать некоторые оптимизации. А именно, мы можем хранить только верхнюю границу каждого интервала в списке. Затем, сохранив отсортированные, мы можем использовать bisect для эффективного поиска.

import bisect

def value_from_hi(hi):
    return r'\{:.1f}'.format(hi - 0.05)

def classify(key, boundaries):
    i = bisect.bisect_right(boundaries, key)
    if i < len(boundaries):
        return value_from_hi(boundaries[i])
    else:
        ... # return some default value

# Sorted upper bounds
boundaries = [-0.85, -0.75, -0.65, -0.55, -0.45, -0.35, -0.25, -0.15, -0.05,
              0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95]

ts_folder = classify(-0.32, boundaries) # r'\-0.3'

Важное примечание: выбор использования более высоких границ и bisect_right обусловлен тем, что в вашем примере исключены более высокие границы. Если нижние границы были исключены, то мы должны были бы использовать те с bisect_left.

Также обратите внимание, что вы можете обрабатывать числа вне диапазона [-0.95, 0,95 [каким-то особым образом) и просто оставьте их для bisect.

Ответ 2

Модуль bisect сделает абсолютно правильный поиск для нахождения правильной корзины из списка точек останова. На самом деле, пример в документации это именно тот случай:

Функция bisect() обычно полезна для классификации числовых данных. В этом примере функция bisect() используется для поиска буквенной оценки итога экзамена (скажем) на основе набора упорядоченных числовых контрольных точек: 85 и выше - это ‘A, 75..84 - это‘ B и т.д.

>>> grades = "FEDCBA"
>>> breakpoints = [30, 44, 66, 75, 85]
>>> from bisect import bisect
>>> def grade(total):
...           return grades[bisect(breakpoints, total)]
>>> grade(66)
'C'
>>> map(grade, [33, 99, 77, 44, 12, 88])
['E', 'A', 'B', 'D', 'F', 'A']

Вместо строки для поиска значений вам нужен список строк с точными именами папок, которые вам нужны для каждого диапазона значений. Например:

breakpoints = [-0.85, -0.75, -0.65]
folders = [r'\-0.9', r'\-0.8', r'\-0.7']
foldername = folders[bisect(breakpoints, -0.72)]

Если вы можете автоматизировать хотя бы часть генерации этой таблицы (используя round() или что-то подобное), конечно, вам следует это сделать.

Ответ 3

Одно из первых правил с таким блоком кода - всегда делать сравнения в одном направлении. Так что вместо

    elif value < -0.75 and value >= -0.85:

написать

    elif -0.85 <= value and value < -0.75:

В этот момент вы можете заметить, что python позволяет создавать цепочки сравнений, поэтому вы можете написать:

    elif -0.85 <= value < -0.75:

Что само по себе является улучшением. Кроме того, вы можете наблюдать это упорядоченный список сравнений, поэтому, если вы добавите в начальные сравнения, вы можете просто написать

    if value < -0.95:        ts_folder = ''
    elif value < -0.85:      ts_folder = r'\-0.9'
    elif value < -0.75:      ts_folder = r'\-0.8'
    elif value < -0.65:      ts_folder = r'\-0.7'    
    elif value < -0.55:      ts_folder = r'\-0.6'   
    elif value < -0.45:      ts_folder = r'\-0.5'  
    elif value < -0.35:      ts_folder = r'\-0.4'
    elif value < -0.25:      ts_folder = r'\-0.3'
    elif value < -0.15:      ts_folder = r'\-0.2'
    elif value < -0.05:      ts_folder = r'\-0.1'
    elif value < 0.05:       ts_folder = r'\0.0'
    elif value < 0.15:       ts_folder = r'\0.1'
    elif value < 0.25:       ts_folder = r'\0.2'
    elif value < 0.35:       ts_folder = r'\0.3'
    elif value < 0.45:       ts_folder = r'\0.4'
    elif value < 0.55:       ts_folder = r'\0.5'
    elif value < 0.65:       ts_folder = r'\0.6'
    elif value < 0.75:       ts_folder = r'\0.7'  
    elif value < 0.85:       ts_folder = r'\0.8'
    elif value < 0.95:       ts_folder = r'\0.9'
    else:                    ts_folder = ''

Это все еще довольно долго, но а) это намного более читабельно; б) он имеет явный код для обработки value < -0.95 or 0.95 <= value

Ответ 4

Вы можете использовать встроенный round():

ts_folder = "\\" + str(round(value + 1e-16, 1)) # To round values like .05 to .1, not .0
if ts_folder == r"\-0.0": ts_folder = r"\0.0" 

Подробнее о round()

Ответ 5

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

ranges = {
    (-0.85, -0.95): r'\-0.9',
    (-0.75, -0.85): r'\-0.8',
    (-0.65, -0.75): r'\-0.7',
    (-0.55, -0.65): r'\-0.6'
    ...
}

def classify (value):
    for (ceiling, floor), rounded_value in ranges.items():
        if floor <= value < ceiling:
            return rounded_value

Выход:

>>> classify(-0.78)
\-0.8

Ответ 6

На самом деле в Python 3 .85 будет округлен до .8. В соответствии с вопросом .85 должен быть округлен до .9.

Можете ли вы попробовать следующее:

round2 = lambda x, y=None: round(x+1e-15, y)
ts_folder = r'\{}'.format(str(round2(value, 1)))

Выход:

>>> round2(.85, 1)
0.9
>>> round2(-.85, 1)
-0.8

Ответ 7

from decimal import Decimal

def classify(value):
    number = Decimal(value)
    result = "%.2f" % (number)
    return Decimal(round(float(result), 2))

Ответ 8

Как насчет того, чтобы превратить его в петлю?

def classify(value):
    i = -5
    while i < 95:
        if value < (i + 10) / 100.0 and value >= i / 100.0:
            return '\\' + repr((i + 5) / 100.0)
        i += 10

это не эффективно ни в коем случае, но это эквивалентно тому, что у вас есть, только короче.

Ответ 9

Вам не нужен and value >= -.85 в elif value < -0.75 and value >= -0.85:; если значение не больше или равно -.85, то вы не достигнете элифа. Вы также можете просто превратить все elif в if, заставив каждый из них немедленно вернуться.

В этом случае, поскольку у вас есть границы через регулярные интервалы, вы можете просто округлить (в общем случае регулярных интервалов, вам, возможно, придется делить, а затем округлять, например, если интервалы находятся в каждых трех единицах, то вы должны делить число на три и выше). В общем случае быстрее сохранить границы в древовидной структуре, а затем выполнить бинарный поиск, где находится элемент.

Явное выполнение бинарного поиска было бы примерно таким:

def classify(value):   
    if value < -.05:
        if value < -.45:
            if value < -.65:
                if value < -.85:
                    if value < -.95:
                        return None
                    return r'\-0.9'
                if value < -.75:
                    return r'\-0.8'
                return r'\-0.7'
    ...

Хотя этот код сложнее для чтения, чем ваш, он выполняется по времени логарифмически, а не линейно по отношению к количеству границ.

Если количество элементов значительно превышает количество границ, вероятно, будет быстрее создать дерево элементов и вставить границы.

Вы также можете создать список, отсортировать его, а затем посмотреть на индекс. Например, сравните (sorted([(_-9.5)/10 for _ in range(20)]+[x]).index(x)-9)/10 с вашей функцией.

Ответ 10

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

  • Представление десятичных значений с плавающей точкой неточно. Например, поплавок 0.85 фактически является 0.8499999999999999777955395....
  • round() использует циклическое округление к четному, также известное как научное или банковское округление, а не арифметическое округление, которое многие из нас изучали в школе. Это означает, например, 0,85 раунда до 0,8 вместо 0,9 и 0,25 раунда до 0,2 вместо 0,3.
  • очень маленькие отрицательные числа с плавающей запятой (и десятичные дроби) округляются до -0.0, а не до 0.0, как требуется для сопоставления OP.

Все они могут быть решены с помощью модуля Decimal, хотя и не так красиво, как хотелось бы:

from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_DOWN

def classify(value):
    number = Decimal('{:.2f}'.format(value))

    if number < 0:
        round_method = ROUND_HALF_DOWN
    else:
        round_method = ROUND_HALF_UP
    rounded_number = number.quantize(Decimal('0.1'), rounding=round_method)

    if rounded_number == 0.0:
        rounded_number = Decimal('0.0')
    return r'\{}'.format(rounded_number)

Требуются и ROUND_HALF_DOWN, и ROUND_HALF_UP, поскольку ROUND_HALF_UP фактически округляется от нуля, а не к бесконечности. .quantize округляет десятичное значение до мест, заданных первым аргументом, и позволяет нам указать метод округления.

Бонус: разделить пополам точки прерывания с помощью range()

Для биссектрисовых решений это создаст точки останова, используемые OP:

from decimal import Decimal
breakpoints = [Decimal('{}e-2'.format(e)) for e in range(-85, 96, 10)]

Ответ 11

Посмотрите на функцию round() в python. Может быть, вы можете обойтись без if.

С помощью этой функции вы можете указать количество цифр, которое вам нужно сохранить. Например:

x = round(5.76543, 2)
print(x)

Этот код напечатает 5,77

Ответ 12

Попробуйте что-то вроде этого, если вам не нравятся циклы:

def classify(value): 
    endpts = [-0.95, -0.85,    -0.75,    -0.65,    -0.55,    -0.45,    -0.35,    -0.25,    -0.15,    -0.05,    0.05,    0.15,    0.25,    0.35,    0.45,    0.55,    0.65,    0.75,    0.85,    0.95] 
    ts_folder = [ r'\-0.9', r'\-0.8', r'\-0.7', r'\-0.6', r'\-0.5', r'\-0.4', r'\-0.3', r'\-0.2', r'\-0.1', r'\0.0', r'\0.1', r'\0.2', r'\0.3', r'\0.4', r'\0.5', r'\0.6', r'\0.7', r'\0.8', r'\0.9'] 
    idx = [value >= end for end in endpts].index(False) 
    if not idx:
        raise ValueError('Value outside of range')
    return ts_folder[idx-1] 

Конечно, цикл просто "скрыт" в понимании списка. Очевидно, что в этом примере было бы лучше генерировать endpts и ts_fol программно, а не записывать их все, но вы указали, что в реальной ситуации конечные точки и значения не так просты.

Это поднимает ValueError, если value ≥ 0,95 (потому что False не найдено в понимании списка) или если value & lt; -0.95 (потому что тогда idx равен 0); в этих случаях оригинальная версия вызывает UnboundLocalError.

Вы также можете сохранить три строки и пропустить несколько сравнений, выполнив следующее:

def classify(value):
    endpts = [-0.95,    -0.85,    -0.75,    -0.65,    -0.55,    -0.45,    -0.35,    -0.25,    -0.15,    -0.05,    0.05,    0.15,    0.25,    0.35,    0.45,    0.55,    0.65,    0.75,    0.85,    0.95]
    ts_fol = [ None, r'\-0.9', r'\-0.8', r'\-0.7', r'\-0.6', r'\-0.5', r'\-0.4', r'\-0.3', r'\-0.2', r'\-0.1', r'\0.0', r'\0.1', r'\0.2', r'\0.3', r'\0.4', r'\0.5', r'\0.6', r'\0.7', r'\0.8', r'\0.9']
    return next((ts for ts, end in zip(ts_fol, endpts) if value < end), None)

Эта версия возвращает None, а не вызывает исключения для любого значения за пределами.