8 нояб. 2010 г.

Python и рекурсия? Увы...

Задача пересчета градусов, выраженных десятичной дробью, в градусы, минуты и секунды для положительного числа x решается тривиально:
  1. Полагаем, что x = исходное число.
  2. Находим целую (i) и дробную (f) часть x.
  3. Целую часть сохраняем как градусы, минуты или секунды.
  4. Если найдены три числа, конец.
  5. Полагаем x = f * 60 и возвращаемся к шагу 2.
Понятно, что эти вычисления производятся в цикле, однако подробности организации цикла пока не важны.
Как быть с отрицательными числами? В этом случае алгоритм немного усложняется:
  1. Полагаем, что x = исходное число.
  2. Находим целую (i) и дробную (f) часть от абсолютного значения x.
  3. Если x положительное, переходим к шагу 5.
  4. Если i не равно 0, полагаем i = -i. В противном случае, f = -f
  5. Целую часть сохраняем как градусы, минуты или секунды.
  6. Если найдены три числа, конец.
  7. Полагаем x = f * 60 и возвращаемся к шагу 2.
Теперь при отрицательном исходном числе, первое значащее число будет отрицательным. Так,
  •  11.75 -- 11:45:0
  • -11.75 -- -11:45:0
  •  -0.75 -- 0:-45:0
Кроме того, удобнее будет секунды возвращать как вещественное, а не целое число, потому что в противном случае при обратном пересчете (градусов, минут и секунд в десятичные градусы) не будет получаться исходное число.
  1. Полагаем, что x = исходное число.
  2. Если при текущем повторении вычисляются секунды, сохраняем x как секунды и выходим из цикла.
  3. Находим целую (i) и дробную (f) часть от абсолютного значения x.
  4. Если x положительное, переходим к шагу 6
  5. Если i не равно 0, полагаем i = -i. В противном случае, f = -f
  6. Целую часть сохраняем как градусы или минуты.
  7. Полагаем x = f * 60 и возвращаемся к шагу 2.
Когда в твоем распоряжении язык высокого уровня можно потратить немало времени в поисках наиболее красивой, эффективной и органичной для языка реализации этого алгоритма. В Python-е имеются как минимум три способа:
  1. рекурсивно;
  2. при помощи циклов: for и while;
  3. с использованием генераторов.
Начнем с рекурсии.



def dms1(x, num_vals=3):

    def mul_60(x, count=1):
        if count == num_vals:
            return [x]
        f, i = math.modf(abs(x))
        if x < 0:
            if i == 0:
                f = -f
            else:
                i = -i
        res = mul_60(f*60.0, count+1)
        res.append(int(i))
        return res

    return tuple(reversed(mul_60(x)))


Функция возвращает кортеж из шестидесятиричных значений: (градусы, минуты, секунды). Именованный параметр num_vals указывает на то, сколько значений необходимо вернуть. Если секунды не нужны, num_vals=2.

Рассмотрим альтернативные способы. Чтобы не повторять один и тот же код, вынесем общую часть в отдельную функцию:

def _next_fi(f):
    negative = f < 0
    f, i = modf(abs(f))
    if negative:
        if i == 0:
            f = -f
        else:
            i = -i
    return (f * 60.0, int(i))

Функция, делающая то же самое при помощи for-цикла, выглядит менее изящно:

def dms2(x, num_vals=3):
    arr = []
    last_j = num_vals - 1
    f = x
    for j in range(0, num_vals):
        if j == last_j:
            arr.append(f)
        else:
            f, i = _next_fi(f)
            arr.append(i)
    return tuple(arr)


А вот вариант с циклом while:

def dms3(x, num_vals=3):
    arr = []
    j = 1
    f = x
    while(j <= num_vals):
        if j == num_vals:
            arr.append(f)
        else:
            f, i = _next_fi(f)
            arr.append(i)
        j += 1
    return tuple(arr)

На очереди -- генераторы. Первый из них использует for, второй -- while

def dms4(x, num_vals=3):
    def mul_60(f):
        last_j = num_vals - 1
        for j in range(0, num_vals):
            if j == last_j:
                yield f
            else:
                f, i = _next_fi(f)
                yield i
    
    return tuple([v for v in mul_60(x)])


def dms5(x, num_vals=3):

    def mul_60(f):

        j = 1
        while(j <= num_vals):
            if j == num_vals:
                yield f
            else:
                f, i = _next_fi(f)
                yield i
            j += 1
    
    return tuple([v for v in mul_60(x)])


Какая из функций работает быстрее всех? Для измерения я воспользовался стандартным методом Timer.timeit, которая по умолчанию запускает заданный код миллион раз. Получилось вот что:

Benchmarking X -> D,M,S

06.465 сек.: 'while'-цикл
07.600 сек.: 'while'-генератор
07.768 сек.: 'for'-цикл
08.544 сек.: 'for'-генератор
09.995 сек.: рекурсия

Увы, самый изящный, на мой взгляд, способ оказался самым медленным! А выиграла самая примитивная функция -- даже не генераторы.

Комментариев нет: