Практика · Числа й арифметика¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
Тут ми руками помацаємо все, про що йшлося в лекції:
- побудуємо простий калькулятор і подивимось на порядок дій;
- перевіримо, що цілі числа в Python справді не мають стелі;
- подивимось, що насправді лежить у пам'яті замість
0.1; - зламаємо порівняння
floatчерез==— і полагодимо його черезmath.isclose; - побачимо, як похибка накопичується в циклі, і як її прибирає
math.fsum; - розберемо банківське округлення;
- порахуємо гроші так, щоб жодна копійка не загубилась.
Зошит виконується згори вниз без жодних правок. Запускай клітинки по черзі.
1 · Калькулятор: сім операторів¶
Почнемо з найпростішого — просто подивимось, що дає кожен оператор на тих самих числах.
Зверни увагу на четвертий рядок: / повертає float навіть тоді, коли ділиться націло.
ділене = 17
дільник = 5
print("ділене =", ділене, " дільник =", дільник)
print()
print("додавання 17 + 5 =", ділене + дільник)
print("віднімання 17 - 5 =", ділене - дільник)
print("множення 17 * 5 =", ділене * дільник)
print("ділення 17 / 5 =", ділене / дільник) # завжди float
print("цілочисельне 17 // 5 =", ділене // дільник)
print("остача 17 % 5 =", ділене % дільник)
print("степінь 17 ** 5 =", ділене ** дільник)
ділене = 17 дільник = 5 додавання 17 + 5 = 22 віднімання 17 - 5 = 12 множення 17 * 5 = 85 ділення 17 / 5 = 3.4 цілочисельне 17 // 5 = 3 остача 17 % 5 = 2 степінь 17 ** 5 = 1419857
Тип результату теж змінюється¶
type(x) показує, до якого типу належить об'єкт. Порівняй 8 / 2 і 8 // 2:
однакове число на екрані — різні типи всередині.
print("8 / 2 ->", 8 / 2, type(8 / 2))
print("8 // 2 ->", 8 // 2, type(8 // 2))
print("2 ** 10 ->", 2 ** 10, type(2 ** 10))
print("2 ** -1 ->", 2 ** -1, type(2 ** -1)) # відʼємний степінь дає float
8 / 2 -> 4.0 <class 'float'> 8 // 2 -> 4 <class 'int'> 2 ** 10 -> 1024 <class 'int'> 2 ** -1 -> 0.5 <class 'float'>
2 · Порядок дій і дужки¶
Три вирази з однаковими числами, але різними дужками. Якщо якийсь результат здивував — відкрий у лекції інтерактив «Дерево обчислення виразу» й набери його там.
print("2 + 3 * 4 =", 2 + 3 * 4) # множення сильніше за додавання
print("(2 + 3) * 4 =", (2 + 3) * 4) # дужки міняють форму дерева
print("2 ** 3 ** 2 =", 2 ** 3 ** 2) # степінь правоасоціативний: 2 ** (3 ** 2)
print("(2 ** 3) ** 2 =", (2 ** 3) ** 2)
print("-2 ** 2 =", -2 ** 2) # унарний мінус слабший за степінь
print("(-2) ** 2 =", (-2) ** 2)
2 + 3 * 4 = 14 (2 + 3) * 4 = 20 2 ** 3 ** 2 = 512 (2 ** 3) ** 2 = 64 -2 ** 2 = -4 (-2) ** 2 = 4
3 · Три ділення й відʼємні числа¶
Ось те, на чому спотикаються навіть досвідчені: // округлює вниз,
а не «до нуля». Перевіримо, що залізна рівність b * (a // b) + (a % b) == a
тримається для всіх комбінацій знаків.
пари = [(7, 2), (-7, 2), (7, -2), (-7, -2)]
print(f"{'a':>4} {'b':>4} | {'a // b':>7} {'a % b':>6} | перевірка b*(a//b) + a%b")
print("-" * 58)
for a, b in пари: # цикл ще попереду за програмою — тут він лише для таблиці
частка = a // b
остача = a % b
print(f"{a:>4} {b:>4} | {частка:>7} {остача:>6} | {b * частка + остача}")
# рівність має виконуватись завжди — це і є визначення // та % у Python
for a, b in пари:
assert b * (a // b) + (a % b) == a, f"рівність зламалась на {a}, {b}"
print()
print("✅ рівність b * (a // b) + (a % b) == a тримається для всіх знаків")
a b | a // b a % b | перевірка b*(a//b) + a%b ---------------------------------------------------------- 7 2 | 3 1 | 7 -7 2 | -4 1 | -7 7 -2 | -4 -1 | 7 -7 -2 | 3 -1 | -7 ✅ рівність b * (a // b) + (a % b) == a тримається для всіх знаків
Остача завжди має знак дільника¶
Саме тому % зручний для циклічних величин: година % 12 завжди дасть 0…11,
навіть якщо година відʼємна.
години = [-25, -13, -1, 0, 11, 25]
for година in години:
print(f"{година:>4} % 12 = {година % 12:>3} divmod: {divmod(година, 12)}")
# int() відрізає до нуля, а // округлює вниз — на відʼємних це різні відповіді
print()
print("int(-3.9) =", int(-3.9), " (відрізає до нуля)")
print("-3.9 // 1 =", -3.9 // 1, " (округлює вниз)")
-25 % 12 = 11 divmod: (-3, 11) -13 % 12 = 11 divmod: (-2, 11) -1 % 12 = 11 divmod: (-1, 11) 0 % 12 = 0 divmod: (0, 0) 11 % 12 = 11 divmod: (0, 11) 25 % 12 = 1 divmod: (2, 1) int(-3.9) = -3 (відрізає до нуля) -3.9 // 1 = -4.0 (округлює вниз)
4 · Цілі числа без стелі¶
Перевіримо на практиці, що int росте скільки треба, а float — ні.
Формула розміру з лекції: 24 + 4 * ceil(бітів / 30) байтів.
import sys
import math
велике = 2 ** 1000
print("2 ** 1000 має", len(str(велике)), "десяткових цифр")
print("двійкових розрядів:", велике.bit_length())
тридцятибітних_цифр = math.ceil(велике.bit_length() / 30)
розмір_за_формулою = 24 + 4 * тридцятибітних_цифр
print("30-бітних цифр:", тридцятибітних_цифр)
print("розмір за формулою:", розмір_за_формулою, "байтів")
print("що каже sys.getsizeof:", sys.getsizeof(велике), "байтів")
assert розмір_за_формулою == sys.getsizeof(велике), "формула розміру не зійшлася"
print()
print("✅ наша формула збіглася з реальним розміром обʼєкта")
2 ** 1000 має 302 десяткових цифр двійкових розрядів: 1001 30-бітних цифр: 34 розмір за формулою: 160 байтів що каже sys.getsizeof: 160 байтів ✅ наша формула збіглася з реальним розміром обʼєкта
А тепер те саме через float¶
2 ** 1000 як ціле — звичайне число. 2.0 ** 1000 як float — на межі можливого,
а 2.0 ** 1024 уже не існує: поле порядку в 11 бітів закінчилось.
print("2.0 ** 1000 =", 2.0 ** 1000)
# показуємо помилку навмисне — traceback теж навчальний матеріал
try:
print(2.0 ** 1024)
except OverflowError as помилка:
print("2.0 ** 1024 -> OverflowError:", помилка)
# а ціле такого розміру Python рахує без жодних скарг
print("2 ** 1024 має", len(str(2 ** 1024)), "цифр і жодних помилок")
2.0 ** 1000 = 1.0715086071862673e+301 2.0 ** 1024 -> OverflowError: (34, 'Numerical result out of range') 2 ** 1024 має 309 цифр і жодних помилок
5 · Що насправді лежить у пам'яті замість 0.1¶
Decimal уміє показати точне значення збереженого числа з рухомою крапкою.
Не округлене, не скорочене — усі цифри до останньої.
from decimal import Decimal
for літерал in ["0.1", "0.2", "0.3", "0.5", "0.75"]:
число = float(літерал)
print(f"{літерал:>5} зберігається як {Decimal(число)}")
0.1 зберігається як 0.1000000000000000055511151231257827021181583404541015625 0.2 зберігається як 0.200000000000000011102230246251565404236316680908203125 0.3 зберігається як 0.299999999999999988897769753748434595763683319091796875 0.5 зберігається як 0.5 0.75 зберігається як 0.75
Бачиш різницю? 0.5 і 0.75 записались точно й коротко — у них знаменник є степенем
двійки. А 0.1, 0.2, 0.3 розтягнулись на 55 цифр: точно їх записати неможливо.
Метод .hex() показує те саме компактніше — мантису й степінь двійки:
print("(0.1).hex() =", (0.1).hex()) # 1.999...a × 2^-4
print("(0.5).hex() =", (0.5).hex()) # 1.0 × 2^-1 — рівно, без хвоста
print()
print("машинний епсилон:", sys.float_info.epsilon)
print("він же 2 ** -52 :", 2.0 ** -52)
(0.1).hex() = 0x1.999999999999ap-4 (0.5).hex() = 0x1.0000000000000p-1 машинний епсилон: 2.220446049250313e-16 він же 2 ** -52 : 2.220446049250313e-16
6 · Порівняння float: як не треба і як треба¶
Спершу зламаємо. Потім полагодимо.
print("0.1 + 0.2 =", 0.1 + 0.2)
print("0.1 + 0.2 == 0.3 ->", 0.1 + 0.2 == 0.3) # False!
print()
print("точна сума ліворуч:", Decimal(0.1 + 0.2))
print("точне значення 0.3:", Decimal(0.3))
print()
print("а от тут пощастило:")
print("0.2 + 0.3 == 0.5 ->", 0.2 + 0.3 == 0.5) # True — похибки погасились
print("0.5 + 0.25 == 0.75 ->", 0.5 + 0.25 == 0.75) # True — обидва числа точні
0.1 + 0.2 = 0.30000000000000004 0.1 + 0.2 == 0.3 -> False точна сума ліворуч: 0.3000000000000000444089209850062616169452667236328125 точне значення 0.3: 0.299999999999999988897769753748434595763683319091796875 а от тут пощастило: 0.2 + 0.3 == 0.5 -> True 0.5 + 0.25 == 0.75 -> True
Правильний інструмент — math.isclose¶
Напишемо власну перевірку «майже рівні» за тією самою ідеєю, що й у стандартній бібліотеці: різниця має бути мала відносно самих чисел. Потім переконаємось, що наша функція дає той самий результат, що й бібліотечна.
def майже_рівні(a, b, відносний_допуск=1e-09):
"""Наша власна версія math.isclose із допуском, пропорційним самим числам."""
# беремо більше з двох за модулем — саме воно задає масштаб допуску
масштаб = max(abs(a), abs(b))
return abs(a - b) <= відносний_допуск * масштаб
перевірки = [
(0.1 + 0.2, 0.3),
(1.0, 1.0000000001),
(1.0, 1.1),
(1e16 + 1, 1e16),
(1000000.0, 1000000.0001),
]
print(f"{'a':>22} {'b':>22} | наша | math.isclose")
print("-" * 70)
for a, b in перевірки:
наш = майже_рівні(a, b)
бібліотечний = math.isclose(a, b)
print(f"{a!r:>22} {b!r:>22} | {str(наш):>5} | {бібліотечний}")
assert наш == бібліотечний, f"розійшлися на {a}, {b}"
print()
print("✅ наша реалізація збіглася з math.isclose на всіх прикладах")
a b | наша | math.isclose
----------------------------------------------------------------------
0.30000000000000004 0.3 | True | True
1.0 1.0000000001 | True | True
1.0 1.1 | False | False
1e+16 1e+16 | True | True
1000000.0 1000000.0001 | True | True
✅ наша реалізація збіглася з math.isclose на всіх прикладах
Пастка з нулем¶
Відносний допуск не працює для порівняння з нулем: у нуля немає масштабу. Для цього випадку допуск задають абсолютний.
крихітне = 0.0000001
print("math.isclose(0.0000001, 0.0) =", math.isclose(крихітне, 0.0))
print("math.isclose(0.0000001, 0.0, abs_tol=1e-06) =", math.isclose(крихітне, 0.0, abs_tol=1e-06))
print()
print("Висновок: порівнюєш із нулем — обовʼязково задавай abs_tol.")
math.isclose(0.0000001, 0.0) = False math.isclose(0.0000001, 0.0, abs_tol=1e-06) = True Висновок: порівнюєш із нулем — обовʼязково задавай abs_tol.
7 · Накопичення похибки¶
Тут нам знадобиться цикл for. Це забігання наперед — циклі докладно
розберемо в темі 12. Поки що читай його так: «повтори те, що всередині, n разів».
def наївна_сума(скільки_разів):
"""Додає 0.1 у циклі — так, як це зробив би будь-хто вручну."""
підсумок = 0.0
for _ in range(скільки_разів):
підсумок = підсумок + 0.1
return підсумок
print(f"{'n':>6} | {'наївний цикл':>22} | {'math.fsum':>22} | збігаються?")
print("-" * 76)
for n in [3, 10, 100, 1000, 10000]:
цикл = наївна_сума(n)
чесна = math.fsum([0.1] * n)
print(f"{n:>6} | {цикл!r:>22} | {чесна!r:>22} | {цикл == чесна}")
n | наївний цикл | math.fsum | збігаються?
----------------------------------------------------------------------------
3 | 0.30000000000000004 | 0.30000000000000004 | True
10 | 0.9999999999999999 | 1.0 | False
100 | 9.99999999999998 | 10.0 | False
1000 | 99.9999999999986 | 100.0 | False
10000 | 1000.0000000001588 | 1000.0 | False
Похибка не випадкова — вона систематично росте. Причина в тому, що кожне збережене
0.1 трохи більше за справжню десятину, тому промахи складаються, а не гасять
одне одного.
Полагодимо це власними руками: реалізуємо компенсоване підсумовування (алгоритм Ноймаєра). Ідея — на кожному кроці запам'ятати «хвостик», який загубився при округленні, і в кінці повернути його назад.
def компенсована_сума(числа):
"""Підсумовує, не втрачаючи молодші розряди: те саме, що робить math.fsum."""
сума = 0.0
компенсація = 0.0 # тут накопичується все, що загубилось при округленні
for число in числа:
нова_сума = сума + число
# який доданок «більший», такий і втрачає молодші біти — звідси дві гілки
if abs(сума) >= abs(число):
компенсація += (сума - нова_сума) + число
else:
компенсація += (число - нова_сума) + сума
сума = нова_сума
return сума + компенсація
набори = [
[0.1] * 10,
[0.1] * 1000,
[1.0, 1e100, 1.0, -1e100], # класичний приклад катастрофічного скорочення
[1e16, 1.0, 1.0, 1.0],
]
for набір in набори:
наша = компенсована_сума(набір)
бібліотечна = math.fsum(набір)
print(f"наша = {наша!r:>22} math.fsum = {бібліотечна!r:>22}")
assert наша == бібліотечна, "компенсована сума розійшлася з math.fsum"
print()
print("✅ наша компенсована сума збіглася з math.fsum на всіх наборах")
наша = 1.0 math.fsum = 1.0 наша = 100.0 math.fsum = 100.0 наша = 2.0 math.fsum = 2.0 наша = 1.0000000000000004e+16 math.fsum = 1.0000000000000004e+16 ✅ наша компенсована сума збіглася з math.fsum на всіх наборах
Важлива чесна дрібниця¶
math.fsum прибирає похибку підсумовування, але не може прибрати похибку, яка
вже сидить у самих числах. Три збережені «десятини» в сумі дають не 0.3 — і жоден
алгоритм тут не допоможе.
І окремо: з Python 3.12 вбудований sum() для float теж компенсує похибку.
Ручний цикл total += 0.1 — ні.
print("math.fsum([0.1] * 3) =", repr(math.fsum([0.1] * 3)))
print("це дорівнює 0.3? ", math.fsum([0.1] * 3) == 0.3)
print()
print("sum([0.1] * 10) =", repr(sum([0.1] * 10))) # з 3.12 компенсоване
print("наївна_сума(10) =", repr(наївна_сума(10))) # звичайний цикл — ні
print()
print("версія Python:", sys.version.split()[0])
math.fsum([0.1] * 3) = 0.30000000000000004 це дорівнює 0.3? False sum([0.1] * 10) = 1.0 наївна_сума(10) = 0.9999999999999999 версія Python: 3.12.2
8 · round() і банківське округлення¶
Реалізуємо правило «половина йде до парного» самі — і перевіримо, що воно
збігається з вбудованим round().
def округлити_до_парного(x):
"""Округлення half-to-even: половинки йдуть до найближчого парного цілого."""
низ = math.floor(x)
дробова = x - низ
if дробова > 0.5:
return низ + 1
if дробова < 0.5:
return низ
# рівно половина: обираємо той із двох сусідів, який парний
return низ if низ % 2 == 0 else низ + 1
print(f"{'x':>6} | {'round(x)':>9} | {'наша':>5} | «шкільне» floor(x+0.5)")
print("-" * 52)
значення = [-2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5, 2.4, 2.6]
for x in значення:
наше = округлити_до_парного(x)
print(f"{x:>6} | {round(x):>9} | {наше:>5} | {math.floor(x + 0.5)}")
assert наше == round(x), f"наше округлення розійшлося з round() на {x}"
print()
print("✅ наша реалізація банківського округлення збігається з round()")
x | round(x) | наша | «шкільне» floor(x+0.5) ---------------------------------------------------- -2.5 | -2 | -2 | -2 -1.5 | -2 | -2 | -1 -0.5 | 0 | 0 | 0 0.5 | 0 | 0 | 1 1.5 | 2 | 2 | 2 2.5 | 2 | 2 | 3 3.5 | 4 | 4 | 4 4.5 | 4 | 4 | 5 2.4 | 2 | 2 | 2 2.6 | 3 | 3 | 3 ✅ наша реалізація банківського округлення збігається з round()
Друга пастка round()¶
round(2.675, 2) дає 2.67, а не 2.68. Це не помилка округлення — це наслідок
того, що збережене 2.675 насправді трохи менше за 2.675.
print("round(2.675, 2) =", round(2.675, 2))
print("а насправді 2.675 зберігається як:")
print(" ", Decimal(2.675))
print()
print("тобто число справді ближче до 2.67 — round() спрацював правильно")
round(2.675, 2) = 2.67 а насправді 2.675 зберігається як: 2.67499999999999982236431605997495353221893310546875 тобто число справді ближче до 2.67 — round() спрацював правильно
9 · Гроші¶
Класична помилка: рахувати ціни через float. Порахуємо чек із трьох позицій
двома способами й подивимось на різницю.
ціни_рядками = ["19.99", "4.35", "0.99"]
# спосіб 1 — через float
сума_float = 0.0
for ціна in ціни_рядками:
сума_float += float(ціна)
# спосіб 2 — через Decimal, створений З РЯДКА (це принципово)
сума_decimal = Decimal("0")
for ціна in ціни_рядками:
сума_decimal += Decimal(ціна)
print("через float :", repr(сума_float))
print("через Decimal:", сума_decimal)
print()
print("точне значення float-суми:", Decimal(сума_float))
print("вони рівні?", Decimal(сума_float) == сума_decimal)
assert сума_decimal == Decimal("25.33"), "Decimal мав дати рівно 25.33"
print()
print("✅ Decimal дав рівно 25.33 — жодного хвоста")
через float : 25.329999999999995 через Decimal: 25.33 точне значення float-суми: 25.329999999999994741983755375258624553680419921875 вони рівні? False ✅ Decimal дав рівно 25.33 — жодного хвоста
Головна помилка з Decimal¶
Decimal(0.1) і Decimal('0.1') — це різні числа. У першому випадку ти передаєш
уже зіпсований float, і Decimal чесно копіює всі його 55 цифр.
з_float = Decimal(0.1)
з_рядка = Decimal("0.1")
print("Decimal(0.1) =", з_float)
print("Decimal('0.1') =", з_рядка)
print("рівні?", з_float == з_рядка)
print()
print("Decimal('0.1') + Decimal('0.2') =", Decimal("0.1") + Decimal("0.2"))
print("це дорівнює Decimal('0.3')?", Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))
Decimal(0.1) = 0.1000000000000000055511151231257827021181583404541015625
Decimal('0.1') = 0.1
рівні? False
Decimal('0.1') + Decimal('0.2') = 0.3
це дорівнює Decimal('0.3')? True
Найпростіший спосіб: цілі копійки¶
Якщо Decimal здається важким — зберігай гроші цілим числом копійок.
Тип int точний за визначенням, і жодних питань про округлення не виникає.
# ті самі три ціни, але в копійках
копійки = [1999, 435, 99]
разом_копійок = sum(копійки)
гривні = разом_копійок // 100 # цілочисельне ділення дає гривні
залишок = разом_копійок % 100 # остача — копійки
print("разом копійок:", разом_копійок)
print(f"це {гривні} грн {залишок} коп")
assert разом_копійок == 2533, "сума в копійках порахована неправильно"
print()
print("✅ цілі копійки збіглися з Decimal-версією (25.33 грн)")
разом копійок:
2533 це 25 грн 33 коп ✅ цілі копійки збіглися з Decimal-версією (25.33 грн)
10 · Fraction: точні дроби¶
Fraction зберігає чисельник і знаменник цілими числами — а вони необмежені.
Тому дріб тут справді дріб, а не наближення до нього.
from fractions import Fraction
# 49 узято не випадково: саме на ньому float спотикається, а на 3 чи 7 — ні
print("Fraction(1, 49) =", Fraction(1, 49))
print("Fraction(1, 49) * 49 =", Fraction(1, 49) * 49)
print("а через float: 1/49*49 =", repr(1 / 49 * 49))
print()
print("Fraction(1, 49) * 49 == 1 ->", Fraction(1, 49) * 49 == 1)
print("1 / 49 * 49 == 1.0 ->", 1 / 49 * 49 == 1.0)
print()
print("Fraction(1, 3) * 3 == 1 ->", Fraction(1, 3) * 3 == 1, " (третина теж точна)")
# Fraction уміє показати, яким дробом насправді є збережений float
print()
print("яким дробом насправді є 0.1:")
print(" ", Fraction(0.1))
print(" (це і є 3602879701896397 / 2**55)")
assert Fraction(0.1) == Fraction(3602879701896397, 2 ** 55), "дріб не збігся"
print()
print("✅ збережене 0.1 — це рівно 3602879701896397 / 2**55")
Fraction(1, 49) = 1/49 Fraction(1, 49) * 49 = 1 а через float: 1/49*49 = 0.9999999999999999 Fraction(1, 49) * 49 == 1 -> True 1 / 49 * 49 == 1.0 -> False Fraction(1, 3) * 3 == 1 -> True (третина теж точна) яким дробом насправді є 0.1: 3602879701896397/36028797018963968 (це і є 3602879701896397 / 2**55) ✅ збережене 0.1 — це рівно 3602879701896397 / 2**55
11 · Модуль math і перетворення типів¶
Останній блок — інструменти, які знадобляться далі щодня.
print("math.pi =", math.pi)
print("math.sqrt(2) =", math.sqrt(2))
print("math.floor(3.7)=", math.floor(3.7), " math.ceil(3.2) =", math.ceil(3.2))
print("math.floor(-3.7)=", math.floor(-3.7), " math.ceil(-3.2) =", math.ceil(-3.2))
print()
print("int('42') + 8 =", int("42") + 8)
print("float('3.5') =", float("3.5"))
print("int(3.9) =", int(3.9), " (відрізає, а не округлює)")
print("round(3.9) =", round(3.9))
# int() від рядка з дробом не працює — це типова помилка новачка
try:
int("3.9")
except ValueError as помилка:
print()
print("int('3.9') -> ValueError:", помилка)
print("правильно так: int(float('3.9')) =", int(float("3.9")))
math.pi =
3.141592653589793
math.sqrt(2) = 1.4142135623730951
math.floor(3.7)= 3 math.ceil(3.2) = 4
math.floor(-3.7)= -4 math.ceil(-3.2) = -3
int('42') + 8 = 50
float('3.5') = 3.5
int(3.9) = 3 (відрізає, а не округлює)
round(3.9) = 4
int('3.9') -> ValueError: invalid literal for int() with base 10: '3.9'
правильно так: int(float('3.9')) = 3
Підсумок¶
Що ми перевірили власними руками:
- рівність
b * (a // b) + (a % b) == aтримається для всіх знаків; - формула розміру цілого
24 + 4 * ceil(бітів / 30)збігається зsys.getsizeof; 2.0 ** 1024переповнюється, а2 ** 1024— ні;- наша
майже_рівнідає те саме, щоmath.isclose; - наша компенсована сума дає те саме, що
math.fsum; - наше округлення до парного дає те саме, що
round; Decimalіз рядка й цілі копійки дають однакову точну суму;- збережене
0.1— це рівно3602879701896397 / 2**55.
Головне, що варто винести: всередині бібліотек немає магії. Там ті самі кілька рядків, які ти щойно написав сам.
Завдання¶
🟢 Рівень 1 — База¶
Порахуй, скільки часу минуло, якщо відомо загальну кількість секунд.
Візьми всього_секунд = 100_000 і виведи результат у форматі
X год Y хв Z с, використавши лише // і %.
Зроблено, якщо: сума години * 3600 + хвилини * 60 + секунди дорівнює
вихідному числу (перевір це через assert).
🟡 Рівень 2 — Плюс¶
Напиши функцію порівняти(a, b), яка приймає два числа й повертає рядок:
"рівні", "майже рівні" або "різні". «Майже рівні» — це коли == каже False,
але math.isclose каже True.
Перевір її щонайменше на п'яти парах, серед яких обовʼязково мають бути
(0.1 + 0.2, 0.3), (0.5 + 0.25, 0.75) і (1.0, 1.1).
Зроблено, якщо: функція повертає "майже рівні" для першої пари,
"рівні" для другої і "різні" для третьої.
🔴 Рівень 3 — Виклик¶
Напиши функцію розкласти_float(x), яка без struct і без Decimal повертає
кортеж (знак, поле_порядку, мантиса) — три цілі числа з внутрішнього
представлення float.
Підказка: math.frexp(x) розкладає число на мантису в діапазоні [0.5, 1) і
показник степеня двійки. Далі мантису треба домножити на 2 ** 53 і врахувати
зсув порядку на 1023.
Зроблено, якщо: для 0.1 функція повертає (0, 1019, 2702159776422298),
а зібране назад значення (-1)**знак * 2**(поле_порядку - 1023) * (1 + мантиса / 2**52) точно дорівнює 0.1 (перевір через ==).