Високосные годы

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

    Желаю, чтобы в конце каждого года ты, вспоминая то, что было за последние 366 if ((year%4 == 0 and year%100 != 0) or (year%400 == 0)) else 365 дней, думал про себя:

    — Ох, нифига себе, какой был экшен. Обязательно буду рассказывать внукам или напишу об этом потом книгу.

    Итак, выше достаточно простой inline-способ определить количество дней в году (переменная year), который, по сути, полностью раскрывает их суть: в григорианском календаре високосными годами считаются те годы, порядковый номер которых либо кратен 4, но при этом не кратен 100, либо кратен 400. Иными словами, если год делится на 4 без остатка, но делится на 100 только с остатком, то он високосный, иначе — невисокосный, кроме случая, если он делится без остатка на 400 — тогда он всё равно високосный.

    Например, 2013 год невисокосный, 1700, 1800 и 1900 — опять же невисокосные годы, а вот 2000, 2004, 2008 и 2012 — високосные.

    Но что, если мы не помним, сколько дней в високосных (366 дней) и невисокосных (365 дней) годах, или просто хотим написать определение количества дней в году максимально быстро? Можно ли сделать так на Python? Конечно же, можно.

    Итак, в Python есть модуль calendar. Он как раз отлично подходит для того, чтобы узнать, является ли тот или иной год високосным (или, например, сколько високосных годов в определённом интервале), определить количество дней в месяце, получить номер дня недели для определённой даты и так далее.

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

    Функция calendar.monthrange принимает номер года в качестве первого аргумента и номер месяца в качестве второго аргумента. Возвращает номер дня недели первого числа данного месяца и количество дней в данном месяце:

    >>> import calendar
    >>> calendar.monthrange(2013, 1)
    (1, 31)
    

    Соответственно, мы можем подсчитать общее количество дней для всех 12 месяцев, и получить таким образом количество дней для данного года:

    >>> import calendar
    >>> year = 2013
    >>> sum(map(lambda x: calendar.monthrange(year, x)[1], range(1, 13)))
    365
    

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

    Проверяем с помощью модуля timeit.

    На то, чтобы выполнить её 1 миллион раз, требуется 13.69 секунд, если import calendar делается один раз в начале. Если import calendar делается каждый раз, тогда 14.49 секунд.

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

    >>> import calendar
    >>> year = 2013
    >>> 365+calendar.isleap(year)
    365
    

    И, как легко догадаться, он уже намного быстрее: 0.83 секунд, включая import calendar, и 0.26 секунд, если import calendar делается один раз в начале.

    Давайте также посмотрим, сколько требуется времени самому первому варианту, с «ручным» подходом: 0.07 секунд для 2012 и 2013 и 0.12 секунд для 2000 (думаю, всем понятно, откуда берётся такая разница в скорости для этих годов).

    Получается, что это и есть самый быстрый вариант из этих трёх:

    >>> import calendar
    >>> year = 2013
    >>> 366 if ((year%4 == 0 and year%100 != 0) or (year%400 == 0)) else 365
    365
    

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

    Пишите, оптимизируйте, улучшайте, тестируйте и считайте производительность — но не забывайте о читаемости исходников ваших программ.

    С Новым годом! Удачи, счастья, радости и самосовершенствования в новом году.
    • +5
    • 41.8k
    • 2
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 2

      0
      А как проверялось с import calendar каждый раз (и зачем вообще оно проверялось)? Просто Python кэширует модули и последующие подключения производятся из словаря. С reload(calendar) (python 2)/from imp import reload; reload(calendar) картина будет совсем другой, хотя что именно следует использовать зависит от того, зачем это вообще проверялось, может надо до кучи ещё и зависимости перезагружать.

      Кстати, сам модуль использует year % 4 == 0 and (year % 100 != 0 or year % 400 == 0).
        0
        Некоторые разработчики помещают импорты непосредственно в тело функции, в которой используется тот или иной модуль, несмотря на то, что PEP 8 советует так не делать (вот ещё обсуждение на SO, в котором даже описывается, в каких случаях это может всё же не являться bad practice).

        В данном тесте сравнивалось лишь время выполнения с импортом непосредственно в тестируемом snippet и время выполнения с импортом, передаваемым в аргументе setup (то, что timeit выполняет один раз).

        Only users with full accounts can post comments. Log in, please.