System binarny
System dziesiętny
Zmiana systemu binarnego na system dziesiętny
Zmiana systemu dziesiętnego na system binarny
Zamiana liczb rzeczywistych z binarnego na dziesiętny
Zamiana liczb rzeczywistych z dziesiętnego na binarny
Cykle
Operacje matematyczne w Python
Pasek postępu
Podsumowanie
Spis treści
Pierwszy raz z systemami binarnymi spotkałem się jakieś 12 lat temu. Wtedy myślałem, że to tylko teoretyczne rozważania i nie mają żadnego odzwierciedlenia w rzeczywistości. Nie zmieniłem zdania nawet na studiach, gdzie uczyłem się szybkiej zamiany między różnymi systemami. Dopiero niedawno jak robiliśmy pasek postępu zauważyłem coś dziwnego. To skłoniło mnie do poszukiwania informacji i dopiero takie połączenie zapaliło u mnie wszystkie lampki i styki, dzięki czemu wiem, żeby unikać liczb zmiennoprzecinkowych kiedy to nie jest niezbędne.
System binarny
Jeśli zajmujesz się programowaniem to jest duża szansa, że spotkałeś się z systemem binarnym, zwanym też systemem dwójkowym. Jest to prosty system w którym podstawą jest liczba 2, a do zapisu używa się jedynie cyfr 0 i 1.
System dziesiętny
Jest to podstawowy system, z którego korzystamy codziennie. Podstawą tego systemu jest liczba 10, a do zapisu używa się cyfr od 0 do 9. Ten system jest dla nas naturalny, komputer operuje na liczbach binarnych, dlatego zanim przejdziemy dalej musimy nauczyć się zamieniac system dziesiętny na binarny i odwrotnie.
Zmiana systemu binarnego na system dziesiętny
Zamiana z liczby binarnej na dziesiętną jest stosunkowo prosta i w dużym uproszczeniu polega na dodaniu kolejnych potęg dwójki licząc od prawej, jeśli cyfra jest jedynką. Rozumiem, że taki teoretyczny opis może być trudny do zrozumienia, dlatego przejdźmy do przykładu.
1011 = 1*2^0 + 1*2^1 + 0*2^2 + 1*2^3 = 1*1 + 1*2 + 0*4 + 1*8 = 1 + 2 + 8 = 11
Dwie kwestie o których ludzie często zapominają (a przynajmniej my zapomnieliśmy w szkole i na studiach ;) )
- idziemy od prawej do lewej
- dowolna liczba do potęgi 0 daje 1
Zróbmy zatem jeszcze kilka przykładów
1000 = 0*2^0 + 0*2^1 + 0*2^2 + 1*2^3 = 8
1100 = 0*2^0 + 0*2^1 + 1*2^2 + 1*2^3 = 4 + 8 = 12
1110 = 0*2^0 + 1*2^1 + 1*2^2 + 1*2^3 = 2 + 4 + 8 = 14
1111 = 1*2^0 + 1*2^1 + 1*2^2 + 1*2^3 = 1 + 2 + 4 + 8 = 15
Zmiana systemu dziesiętnego na system binarny
Żeby skutecznie zamieniać liczby z systemu dziesiętnego na system binarny musimy cofnąć się pamięcią do szkoły podstawowej w której uczyliśmy się dzielenia z resztą. Zmiana polega bowiem na dzieleniu liczby przez dwa i zapisywaniu reszty. Całość powtarzamy aż nie będzie czego dzielić. Praktyka ponownie jest znacznie łatwiejsza niż teoria. Dlatego przejdźmy do kilku praktycznych przykładów
11 : 2 = 5 r 1
5 : 2 = 2 r 1
2 : 2 = 1 r 0
1 : 2 = 0 r 1
= 1011
Teraz idąc od dołu zapisujemy wynik, przez co otrzymujemy wynik 1011.
Tu dwie ważne uwagi
- Wynik z dzielenia jest kolejną liczbą, którą będziemy dzielić
- wynik czytamy od dołu do góry
Przejdźmy do kolejnego przykładu i sprawdźmy czy wyliczona wcześniej 14 faktycznie była poprawnie zamieniona
14 : 2 = 7 r 0
7 : 2 = 3 r 1
3 : 2 = 1 r 1
1 : 2 = 0 r 1
= 1110
Zamiana liczb rzeczywistych z binarnego na dziesiętny
Jeśli zrozumiałeś dokładnie zamianę liczb naturalnych, to możemy przejść dalej. Tu będzie nieco więcej zabawy. Zamiana z systemy binarnego, do systemu dziesiętnego jest analogiczna ale tu idziemy od lewej do prawej i mnożymy przez kolejne ujemne ułamki liczby dwa. Ja zawsze rozbijam operację liczenia oddzielnie na część całkowitą i część po przecinku. Zatem przejdźmy do przykładu
10.011
10. = 0 * 2^1 + 1*2^1 = 0 + 2 = 2.
.011 = 0 * 2^(-1) + 1 * 2^(-2) + 1 * 2(-3) = 0 + 1/4 + 1/8 = 3/8 = .375
To w rezultacie daje nam wynik 2.375.
Z ważnych uwag
- Ujemne potęgi liczby możemy potraktować jako odwrotny ułamek, czyli 2^(-2) = 1/(2^2) = 1/4
- Tu liczmy od lewej do prawej
Zróbmy jeszcze z jeden przykład. Dla uproszczenia pomińmy część całkowitą
.101 = 1*2^(-1) + 0*2^(-2) + 1*2^(-3) = 1/2 + 1/8 = 4/8 + 1/8 = 5/8 = 0.625
Zamiana liczb rzeczywistych z dziesiętnego na binarny
To była ta operacja, którą najtrudniej było mi z początku zrozumieć. Musimy bowiem część po przecinku potraktować oddzielnie i kolejne liczby mnożyć. Zobaczmy na przykładzie
.625 * 2 = 1.25 # część całkowita nas nie interesuje
.25 * 2 = 0.5
.5 * 2 = 1.0
Idąc od góry do dołu mamy wynik 101
Z ważnej uwagi mogę jedynie dodać, że tu idziemy od góry do dołu, w przeciwieństwie do zamiany liczb całkowitych w których wynik zbieraliśmy od dołu. Sprawdźmy może kolejny przykład
.375 * 2 = 0.75
.75 * 2 = 1.5
.5 * 2 = 1.0
Wynik zgadza się z tym co było wcześniej, czyli .375 dziesiętnie to .011 binarnie.
Cykle
Jak pewnie zauważyłeś zamiana liczb rzeczywistych z systemu dziesiętnego na binarny nie jest taka prosta. A to jeszcze nie wszystko. Problem pojawia się jeśli wynik nie będzie chciał się tak ładnie zakończyć. Weźmy liczbę, który dla nas wygląda bardzo prosto, czyli 0.2
.2 * 2 = 0.4
.4 * 2 = 0.8
.8 * 2 = 1.6 # część dziesiętna nas nie interesuje
.6 * 2 = 1.2
.2 * 2 = 0.4 # cykl
.4 * 2 = 0.8
.8 * 2 = 1.6 # część dziesiętna nas nie interesuje
.6 * 2 = 1.2
...
Jak widać taka prosta liczba jest trudna do przeliczenia. Jak zatem to zapisać? Sięgnijmy do okresowych rozwinięć, czyli 0.2 zamieniona z systemu dziesiętnego na system binarny daje 0.(0011). Przeliczmy zatem kilka takich cykli
.0011 = 0 + 0 + 1/8 + 1/16 = 3/16 = 0.1875
.00110011 = 0 + 0 + 1/8 + 1/16 + 0 + 0 + 1/128 + 1/256 = 51/256 = 0.19921875
.001100110011 = 1/8 + 1/16 + 1/128 + 1/256 + 1/2048 + 1/4096 = 819/4096 = 0.199951171875
Jak widzimy operacje przy 12 bitach są już trudne do łatwego liczenia. Jest to równocześnie dość dobre przybliżenie. No dobra ale jak to ma się do praktyki i programowania, a tym bardziej programowania w Python? Zobaczmy na przykładzie.
Operacje matematyczne w Python
3/10 # 0.3
2/10 # 0.2
1/10 # 0/1
0.2 + 0.1 # 0.30000000000000004
Ten przykład podawałem we wpisie o dziwnych przypadkach w Python . Co tu się dokładnie stało? A no stało się właśnie przybliżenie. Domyślnie liczby zmiennoprzecinkowe są typu float, które mają aż albo tylko 64 bity do reprezentacji liczby.
Pasek postępu
Mamy za sobą masę teorii i trochę praktyki. Teraz chciałbym pokazać Wam co stało się, że postanowiłem napisać ten wpis. Otóż podczas pisania prostego procentowego paska postępu zauważyłem, że pojawiały się jakieś dziwne cyfry. Zbliżyłem temat i napisałem skrypt, który widzimy poniżej.
x = 0
while x!= 10:
x += 1
print(x/10)
Skrypt wydawał się w porządku ale irytowało nas dzielenie przez 10, więc zamieniliśmy to właśnie na liczby rzeczywiste
x = 0
while x!= 1.0:
x += .1
print(x)
Okazało się, że skrypt wpadł w nieskończoną pętlę, bo wynik nigdy nie był równy 1.0.
# 0.1
# 0.2
# 0.30000000000000004
# 0.4
# 0.5
# 0.6
# 0.7
# 0.7999999999999999
# 0.8999999999999999
# 0.9999999999999999
# 1.0999999999999999
# 1.2
# 1.3
# 1.4000000000000001
# 1.5000000000000002
# 1.6000000000000003
# 1.7000000000000004
# 1.8000000000000005
# 1.9000000000000006
# 2.0000000000000004
Oczywiście prostym poprawieniem była zmiana znaku z != na <= , ale świadomy programista wyciąga lekcje ze swoich błędów. Dlatego zacząłem to badać i wypisałem więcej cyfr niż tylko te widoczne na ekranie. Całość ułatwiły mi f-stringy
x = 0.0
while x<= 1.0:
print(f"{x:.50f}")
x += 0.1
# 0.00000000000000000000000000000000000000000000000000
# 0.10000000000000000555111512312578270211815834045410
# 0.20000000000000001110223024625156540423631668090820
# 0.30000000000000004440892098500626161694526672363281
# 0.40000000000000002220446049250313080847263336181641
# 0.50000000000000000000000000000000000000000000000000
# 0.59999999999999997779553950749686919152736663818359
# 0.69999999999999995559107901499373838305473327636719
# 0.79999999999999993338661852249060757458209991455078
# 0.89999999999999991118215802998747676610946655273438
# 0.99999999999999988897769753748434595763683319091797
Jak widzimy wyniki nie są zbyt dokładne, dodatkowo zauważyłem że coś za długi ten wynik. Miało być 10 powtórzeń. Dodałem więc jeszcze zmienną pomocniczą.
x = 0.0
i = 0
while x<= 1.0:
print(f"{x:.50f}")
x += 0.1
i += 1
# 0 0.00000000000000000000000000000000000000000000000000
# 1 0.10000000000000000555111512312578270211815834045410
# 2 0.20000000000000001110223024625156540423631668090820
# 3 0.30000000000000004440892098500626161694526672363281
# 4 0.40000000000000002220446049250313080847263336181641
# 5 0.50000000000000000000000000000000000000000000000000
# 6 0.59999999999999997779553950749686919152736663818359
# 7 0.69999999999999995559107901499373838305473327636719
# 8 0.79999999999999993338661852249060757458209991455078
# 9 0.89999999999999991118215802998747676610946655273438
# 10 0.99999999999999988897769753748434595763683319091797
Zwróćmy uwagę na wiersze 5 i 6. Te przybliżenie było dla mnie największą zagadką. Dlatego właśnie powinniśmy korzystać z Decimal, który jest znacznie dokładniejszy. Nie jest to idealne przybliżenie, ale dwa razy więcej bitów pozwalają na dużo precyzyjniejsze wypisanie liczby. Poniżej zaprezentuję Wam poprawiony skrypt o uwzględniony właśnie Decimal
from decimal import Decimal
i = 0
x = Decimal(0.0)
while x<= 1.0:
print(f"# {i} \t {x:.50f}")
x += Decimal(0.1)
i += 1
# 0 0.00000000000000000000000000000000000000000000000000
# 1 0.10000000000000000555111512310000000000000000000000
# 2 0.20000000000000001110223024620000000000000000000000
# 3 0.30000000000000001665334536930000000000000000000000
# 4 0.40000000000000002220446049240000000000000000000000
# 5 0.50000000000000002775557561550000000000000000000000
# 6 0.60000000000000003330669073860000000000000000000000
# 7 0.70000000000000003885780586170000000000000000000000
# 8 0.80000000000000004440892098480000000000000000000000
# 9 0.90000000000000004996003610790000000000000000000000
Jak widzimy przybliżenia są już znacznie dokładniejsze i najważniejsze mamy dokładnie 10 iteracji, tak jak oczekiwaliśmy.
Podsumowanie
To był bardzo szybki, aczkolwiek bardzo wyczerpujący i treściwy wpis. Liczę się z tym, że wielu czytelników będzie miało problem z dotarciem do samego końca. Jeśli Ty nie zrozumiałeś za dużo, to nic nie szkodzi. Wróć tu za jakiś czas i daj znać czy po jakimś czasie jest lepiej. Do pełnego zrozumienia tego wpisu potrzebna jest ogólna wiedza z zakresu działania komputerów oraz pewne zacięcie, które sam widzę stosunkowo rzadko.
Jeśli miałbym wyciągnąć z tego wpisu trzy najważniejsze kwestie, to byłyby to:
- Teoria i ogólna wiedza informatyczna jest bardziej przydatna niż wygląda to na pierwszy rzut oka
- Jeśli nie musisz, nie używaj typów zmiennoprzecinkowych
- Jeśli musisz korzystać z liczb rzeczywistych i zależy Ci na precyzji to zapoznaj się z Decimal