Γιατί κολλάς ενώ έχεις κάνει δέκα tutorials
    Το περιβάλλον σου
    Δεδομένα & τύποι

    0.1 + 0.2 δεν κάνει 0.3

    Ίδιο περιεχόμενο δεν σημαίνει ίδιο πράγμα

    Λογική & ροή
    Συναρτήσεις
    Δομές δεδομένων
    Errors & debugging
    Είσοδος, έξοδος & JSON
    Οργάνωση κώδικα & αντικείμενα
    Εξαρτήσεις & πακέτα
    Τεστ
    Project 1: το εργαλείο
    Τι σημαίνει backend service
    Το πρώτο σου API
    Συμβόλαια, validation & σφάλματα
    Βάσεις δεδομένων
    Auth & ασφάλεια
    Cache & background jobs
    Ταυτοχρονισμός
    Κώδικας που πάει production
    Project 2: το service
Απορίες;

0.1 + 0.2 δεν κάνει 0.3

Υπάρχει ένα bug που εμφανίζεται σε κάθε εταιρεία που χρεώνει πελάτες, γράφεται από ανθρώπους που ξέρουν να προγραμματίζουν, και ανακαλύπτεται συνήθως από το λογιστήριο. Τα σύνολα δεν βγαίνουν. Λείπει ένα λεπτό.
Η αιτία δεν είναι λάθος στη λογική. Είναι ότι ο υπολογιστής δεν μπορεί να γράψει το 0,10 ευρώ.

Δύο είδη αριθμών, μία διαφορά που μετράει

Η Python έχει ακέραιους και δεκαδικούς, int και float. Τους ξεχωρίζει η τελεία:
python
quantity = 5
unit_price = 41.60
Οι πράξεις είναι όπως τις ξέρεις, με μία έκπληξη:
python
print(10 / 5)
Τυπώνει 2.0, όχι 2. Η διαίρεση με / επιστρέφει πάντα δεκαδικό, ακόμα και όταν βγαίνει τέλεια. Αν θέλεις ακέραιο πηλίκο, υπάρχει άλλος τελεστής:
python
print(7 // 2)
print(7 % 2)
3 και 1. Το // δίνει το ακέραιο πηλίκο και το % το υπόλοιπο. Χρειάζονται πιο συχνά από όσο φαντάζεσαι: πόσες πλήρεις συσκευασίες των 6 βγαίνουν από 20 τεμάχια και πόσα περισσεύουν, σε ποια δόση πέφτει ένα ποσό, αν ένας αριθμός είναι ζυγός.
Στους αρνητικούς το // δεν κόβει προς το μηδέν, στρογγυλοποιεί προς τα κάτω: το -7 // 2 δίνει -4, όχι -3.
Και μια ευχάριστη διαφορά από τις περισσότερες γλώσσες: ο ακέραιος στην Python δεν έχει ταβάνι.
python
print(2 ** 200)
Τυπώνει έναν αριθμό με 61 ψηφία, σωστό μέχρι το τελευταίο. Στις περισσότερες άλλες γλώσσες ο ίδιος υπολογισμός με ακέραιους ξεχειλίζει και σου δίνει σκουπίδια, χωρίς καμία προειδοποίηση.
Οι ακέραιοι δηλαδή είναι ακριβείς, πάντα. Το πρόβλημα ζει αποκλειστικά στους δεκαδικούς.

Η προδοσία

Ο λογαριασμός ρεύματος έχει δύο χρεώσεις ανά κιλοβατώρα: την ενέργεια και το δίκτυο. Πρόσθεσέ τες.

Πρόβλεψε

Διάλεξε μία
energy = 0.10
network = 0.20
total = energy + network
print(total)
print(total == 0.30)

Τι τυπώνει, με αυτή τη σειρά;

Γιατί συμβαίνει;

Δεν είναι bug της Python. Το ίδιο ακριβώς γίνεται σε Java, σε C, σε JavaScript και στο Excel. Είναι ο τρόπος που ο επεξεργαστής αποθηκεύει δεκαδικούς, και τον μοιράζονται όλοι.
Γράψε το ένα τρίτο ως δεκαδικό. Γράφεις 0,333 και σου λείπει λίγο. Γράφεις 0,3333333 και πάλι σου λείπει. Όσα ψηφία και να βάλεις, δεν φτάνεις ποτέ, γιατί το 1/3 δεν γράφεται με πεπερασμένα ψηφία στη βάση 10.
Ο υπολογιστής δεν δουλεύει στη βάση 10, δουλεύει στο δυαδικό. Και στο δυαδικό, το ένα δέκατο είναι ακριβώς ό,τι είναι το ένα τρίτο για εμάς: ατελείωτο. Αποθηκεύει λοιπόν το κοντινότερο που χωράει σε 64 bit. Ρώτα τον τι κρατάει πραγματικά:
python
print(format(0.1, ".20f"))
text
0.10000000000000000555
Αυτό είναι το «0.1» σου. Δεν είναι το ένα δέκατο, είναι λίγο παραπάνω. Κάνε δύο τρεις πράξεις με τέτοιους αριθμούς και το λάθος βγαίνει στην επιφάνεια.
Το ύπουλο είναι ότι δεν βγαίνει πάντα:
python
print(5.35 * 5)
print(5.35 * 3)
text
26.75
16.049999999999997
Ίδια τιμή, ίδιος πολλαπλασιασμός, δύο εντελώς διαφορετικές εντυπώσεις. Η πρώτη γραμμή σε καθησυχάζει και η δεύτερη σε προδίδει. Δεν είναι ότι οι δεκαδικοί βγαίνουν πάντα λάθος. Είναι ότι δεν ξέρεις πότε, και δεν θα το ελέγχεις κάθε φορά.

Πού κοστίζει

Στην απόδειξη ενός πελάτη δεν κοστίζει τίποτα, γιατί θα τυπώσεις δύο δεκαδικά και το 16.049999999999997 θα γίνει 16.05.
Κοστίζει όταν αθροίζεις. Ένα e-shop με 40.000 παραγγελίες τον μήνα βγάζει ένα σύνολο που δεν συμφωνεί με το άθροισμα των αποδείξεων. Η διαφορά είναι μερικά λεπτά, οπότε κανείς δεν την προσέχει για καιρό, και όταν τελικά τη δουν, κάποιος θα κληθεί να την ψάξει. Κοστίζει επίσης όταν συγκρίνεις: ο έλεγχος «πλήρωσε ακριβώς όσο χρωστούσε» βγαίνει ψευδής για δύο ποσά που στα χαρτιά είναι ίδια.
Και κοστίζει στις στρογγυλοποιήσεις, με τρόπο που δεν τον περιμένεις:
python
print(round(2.675, 2))
Τυπώνει 2.67. Όχι επειδή η round είναι χαλασμένη, αλλά επειδή ο αριθμός που της έδωσες ήταν ήδη λίγο κάτω από το 2,675 πριν καν τον δει.
Η round έχει και μια δεύτερη συμπεριφορά που ξαφνιάζει: το round(0.5) δίνει 0 και το round(2.5) δίνει 2. Στα μισά στρογγυλοποιεί προς τον ζυγό, όχι πάντα προς τα πάνω.
Είναι σκόπιμο και είναι το πρότυπο που ακολουθεί όλος ο κλάδος, γιατί το «πάντα προς τα πάνω» φουσκώνει συστηματικά τα αθροίσματα.

Για χρήματα υπάρχει σωστό εργαλείο

Η standard library έχει έναν τύπο που κάνει ακριβώς τις πράξεις που θα έκανες με μολύβι, στη βάση 10.
python
from decimal import Decimal

energy = Decimal("0.10")
network = Decimal("0.20")
print(energy + network == Decimal("0.30"))
Τυπώνει True. Το from decimal import Decimal είναι μια γραμμή που φέρνει ένα εργαλείο από την τυπική βιβλιοθήκη της Python, δηλαδή από κώδικα που ήρθε μαζί με τον interpreter και δεν χρειάζεται να τον εγκαταστήσεις.
Ένας κανόνας και μία παγίδα.
Ο κανόνας: φτιάχνεις Decimal πάντα από string, ποτέ από δεκαδικό αριθμό. Το Decimal("0.1") είναι ακριβώς ένα δέκατο. Το Decimal(0.1) παίρνει τον ήδη χαλασμένο δεκαδικό και τον αντιγράφει με όλη του τη λάσπη:
text
Decimal('0.1000000000000000055511151231257827021181583404541015625')
Η παγίδα, που είναι στην πραγματικότητα προστασία: η Python αρνείται να ανακατέψει τα δύο είδη.
python
print(Decimal("0.10") + 0.20)
text
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'
Καλύτερα να κρασάρει εκεί, παρά να σου δώσει έναν αριθμό που μοιάζει σωστός. Οι πράξεις με ακεραίους επιτρέπονται κανονικά, οπότε το Decimal("5.35") * 5 δουλεύει και δίνει ακριβώς 26.75.
Υπάρχει και δεύτερος δρόμος, που τον χρησιμοποιούν πολλά συστήματα πληρωμών: κρατάς τα πάντα σε λεπτά, δηλαδή σε ακέραιους. Το προϊόν κοστίζει 535, όχι 5,35. Οι ακέραιοι είναι ακριβείς, οπότε το πρόβλημα εξαφανίζεται, με κόστος ότι πρέπει να θυμάσαι να διαιρείς με το 100 σε κάθε εμφάνιση.
Για ό,τι δεν είναι χρήμα, οι δεκαδικοί είναι μια χαρά. Μέσος όρος αξιολογήσεων, ποσοστό ολοκλήρωσης, θερμοκρασία, χιλιόμετρα. Εκεί το δέκατο έβδομο δεκαδικό ψηφίο δεν ενδιαφέρει κανέναν.

Σύνοψη

  • int και float. Η / επιστρέφει πάντα δεκαδικό, το // δίνει ακέραιο πηλίκο και το % το υπόλοιπο.
  • Ο ακέραιος στην Python δεν ξεχειλίζει ποτέ και είναι πάντα ακριβής. Όλη η ανακρίβεια ζει στους δεκαδικούς.
  • Το 0.1 δεν υπάρχει στη μνήμη. Υπάρχει ο κοντινότερος δυαδικός αριθμός, και γι' αυτό το 0.1 + 0.2 δίνει 0.30000000000000004. Ισχύει σε κάθε γλώσσα, όχι μόνο εδώ.
  • Ποτέ == ανάμεσα σε δεκαδικούς, γιατί ρωτάς για ακριβή ισότητα αριθμούς που είναι προσεγγίσεις.
  • Το λάθος δεν εμφανίζεται πάντα. Το 5.35 * 5 βγαίνει καθαρό και το 5.35 * 3 όχι, οπότε δεν μπορείς να βασιστείς στο ότι το είδες μια φορά να δουλεύει.
  • Για χρήματα, Decimal από string ή ακέραια λεπτά. Ποτέ Decimal από δεκαδικό, γιατί αντιγράφεις το ίδιο σφάλμα σε ακριβότερο τύπο.

Θες να συνεχίσεις;

Αυτό το μάθημα είναι δωρεάν. Κάνε σύνδεση για να κρατάς την πρόοδο και τις σημειώσεις σου, ή απόκτησε το course για να ξεκλειδώσεις τα υπόλοιπα μαθήματα, τα labs και την κοινότητα.