Определение високосного года

Изучаю питон минут 20 и столкнулся с такой задачей:

Дано натуральное число. Требуется определить, является ли год с данным номером високосным. Если год является високосным, то выведите YES, иначе выведите NO. Напомним, что в соответствии с григорианским календарем, год является високосным, если его номер кратен 4, но не кратен 100, а также если он кратен 400.

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

Подскажите пожалуйста, оно ведь не должно работать?

a = int(input())
if a % 100 != 0 and a % 4 == 0 or a % 400 == 0:
    print('YES')
else:
    print('NO')

Ответы (4 шт):

Автор решения: CrazyElf
if a % 100 != 0 and a % 4 == 0 or a % 400 == 0:

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

год является високосным, если его номер кратен 4, но не кратен 100, а также если он кратен 400.

Запишем именно в таком же порядке. Это не влияет на выполнение условия, но влияет на наше понимание логики.

if a % 4 == 0 and a % 100 != 0 or a % 400 == 0:

Всё в точности как в условии:

  • номер кратен 4 -> a % 4 == 0
  • но (при этом) -> and
  • не кратен 100 -> a % 100 != 0
  • а также если -> or
  • он кратен 400 -> a % 400 == 0

Тут нужно пояснить, что:

  • Логическое умножение and имеет больший приоритет, чем сложение or, поэтому тут как бы подразумеваются такие скобки: if (a % 4 == 0 and a % 100 != 0) or a % 400 == 0:
  • Фраза "кратен ... но не кратен ..." имеет в виду одновременное выполнение условий, т.е. and
  • Фраза "а также если" в данном случае подразумевает "в случае если ... условие тоже выполняется", т.е. or
→ Ссылка
Автор решения: Дмитрий

Вариант поинтереснее ). Без этих чудовищных ветвлений

def leap_year(year: int):
    if year % 4 != 0:
        return False
     elif year % 100 == 0:
        if year % 400 == 0:
            return True
        else:
            return False
     else:
         return True


year = int(input())
if leap_year(year):
    print("YES")
else:
    print("No")
→ Ссылка
Автор решения: mrgervant

Определение високосности года через побитовое И:

def is_leap_year(year):
    return ((year * 1073750999) & 3221352463) <= 126976

Указанные "магические числа" корректно работают для диапазона от 0 до 102499 года.

Автор данного решения - Falk Hüffner (перевод на русский) . По данным ссылкам доступно объяснение, как именно было найдено это решение - оно довольно большое и сложное.

По большей части оно найдено перебором с помощью библиотеки Z3. Falk Hüffner искал подходящие константы для максимального покрытия в рамках 32-битных чисел, но с его кодом можно найти подходящее решение для диапазона от 0 до 9999 года - это более оптимальный период, т.к. по григорианскому календарю ошибка копится за ~10.000 лет (в том же Python datetime.MAXYEAR = 9999):

import z3

MAX_YEAR = 9999

BITS = 32
f, m, t, y = z3.BitVecs('f m t y', BITS)

def target(y):
    return z3.And((y & 3) == 0, z3.Or(z3.URem(y, 25) != 0, (y & 15) == 0))

def candidate(x):
    return z3.ULE((x * f) & m, t)

solver = z3.Solver()
solver.add(z3.ForAll(y, z3.Implies(z3.ULE(y, MAX_YEAR),
                                   candidate(y) == target(y))))

if solver.check() == z3.sat:
    print(f'found solution: {solver.model()}')
else:
    print('no solution found')

Вывод:

[f = 1073836193, m = 3222241295, t = 1015808]

Соответственно, функция для проверки високосности до 9999 года:

def is_leap_year(year):
    return ((year * 1073836193) & 3222241295) <= 1015808
→ Ссылка
Автор решения: Serge3leo

Хм, удивительно, но похоже Python любит if и не любит формулы.

Самая длинная, в исходном коде, функция от @Дмитрий https://ru.stackoverflow.com/a/1614021/430734 оказалась самой шустрой.

Понятно, что если if y % 4 != 0: и if y % 100 != 0: заменить на if y % 4: и if y % 100: станет ещё шустрее ≈7%, но это уже "задний ум", не считается. ?

P.S.

Плюс, по совету @Stanislav Volodarskiy, замена if y % 4: на if y & 3: даёт ещё ≈9%. И, как следствие, замена y % 4 == 0 на not (y & 3) в логической формуле тоже даёт выигрыш.

test_years = list(range(10_000))
leaps = []
def leap_Catik(y: int) -> bool:
    return y % 100 != 0 and y % 4 == 0 or y % 400 == 0
leaps.append(leap_Catik)
def leap_CrazyElf(y: int) -> bool:
    return y % 4 == 0 and y % 100 != 0 or y % 400 == 0
leaps.append(leap_CrazyElf)
def leap_CrazyElf_1(y: int) -> bool:
    return (y % 4 == 0 and y % 100 != 0) or y % 400 == 0
leaps.append(leap_CrazyElf_1)
def leap_Дмитрий(y: int) -> bool:
    if y % 4 != 0:
        return False
    elif y % 100 == 0:
        if y % 400 == 0:
            return True
        else:
            return False
    else:
        return True
leaps.append(leap_Дмитрий)
def leap_Stanislav_Volodarskiy(y: int) -> bool:
    if y % 400 == 0: return True
    if y % 100 == 0: return False
    return y % 4 == 0
leaps.append(leap_Stanislav_Volodarskiy)
def leap_Serge3leo(y: int) -> bool:
    return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)
leaps.append(leap_Serge3leo)
def leap_mrgervant(y: int) -> bool:
    return ((y * 1073750999) & 3221352463) <= 126976
leaps.append(leap_mrgervant)
assert not leaps[0](1899)
assert not leaps[0](1900)
assert leaps[0](1980)
assert leaps[0](2000)
for lf in leaps[1:]:
    for y in test_years:
        assert leaps[0](y) == lf(y)
for lf in leaps:
    print(f"{lf.__name__}:   Длина байткода={len(lf.__code__.co_code)}")
    %timeit for y in test_years: lf(y)
    %timeit lf(2025)

Время и размер байт-кода для Python 3.13:

leap_Catik:   Длина байткода=78
1.72 ms ± 2.49 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
172 ns ± 0.137 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_CrazyElf:   Длина байткода=78
1.43 ms ± 1.6 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
134 ns ± 0.399 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_CrazyElf_1:   Длина байткода=78
1.43 ms ± 3.49 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
134 ns ± 0.317 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_Дмитрий:   Длина байткода=64
986 μs ± 1.06 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
85.3 ns ± 0.475 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_Stanislav_Volodarskiy:   Длина байткода=58
1.72 ms ± 3.09 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
159 ns ± 0.351 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_Serge3leo:   Длина байткода=78
1.03 ms ± 1.04 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
88.4 ns ± 0.212 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_mrgervant:   Длина байткода=24
2 ms ± 5.34 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
195 ns ± 0.179 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

Сравнение быстрых if с быстрыми формулами:

leaps = []
def leap_быстрый_if(y: int) -> bool:
    if y & 3:
        return False
    elif y % 100:
        return True
    return y % 400 == 0        
leaps.append(leap_быстрый_if)
def leap_быстрый_fif(y: int) -> bool:
    return False if y & 3 else True if y % 100 else y % 400 == 0
leaps.append(leap_быстрый_fif)
def leap_быстрый_формула(y: int) -> bool:
    return not (y & 3) and (y % 100 != 0 or y % 400 == 0)
leaps.append(leap_быстрый_формула)
for lf in leaps:
    for y in test_years:
        assert leap_Serge3leo(y) == lf(y)
for lf in leaps:
    print(f"{lf.__name__}:   Длина байткода={len(lf.__code__.co_code)}")
    %timeit for y in test_years: lf(y)
    %timeit lf(2025)

Время и размер байт-кода для Python 3.13:

leap_быстрый_if:   Длина байткода=62
852 μs ± 520 ns per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
71.8 ns ± 0.0888 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_быстрый_fif:   Длина байткода=66
875 μs ± 11.6 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
73.4 ns ± 0.237 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_быстрый_формула:   Длина байткода=82
925 μs ± 2.23 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
76.2 ns ± 0.193 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
→ Ссылка