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

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

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

Ответы [ 12 ]

44 голосов
/ 15 марта 2019

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

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

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.

25 голосов
/ 15 марта 2019

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

Функция 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() или что-то подобное), конечно, вам следует.

16 голосов
/ 15 марта 2019

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

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

write

    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

11 голосов
/ 15 марта 2019

Все ответы вращаются вокруг округления, что, кажется, хорошо в этом случае, но просто ради аргумента я хотел бы также отметить классное использование словарей на 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
11 голосов
/ 15 марта 2019

Вы можете использовать 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 голосов
/ 15 марта 2019

На самом деле в 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
3 голосов
/ 15 марта 2019

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

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

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

3 голосов
/ 15 марта 2019
from decimal import Decimal

def classify(value):
    number = Decimal(value)
    result = "%.2f" % (number)
    return Decimal(round(float(result), 2))
2 голосов
/ 17 марта 2019

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

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

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

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)]
2 голосов
/ 15 марта 2019

Вам не нужно 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 с вашей функцией.

...