Γιατί κολλάς ενώ έχεις κάνει δέκα tutorials
Το περιβάλλον σου
Δεδομένα & τύποι
Λογική & ροή
Συναρτήσεις
Δομές δεδομένων
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. Τους ξεχωρίζει η τελεία:pythonquantity = 5 unit_price = 41.60
Οι πράξεις είναι όπως τις ξέρεις, με μία έκπληξη:
pythonprint(10 / 5)
Τυπώνει
2.0, όχι 2. Η διαίρεση με / επιστρέφει πάντα δεκαδικό, ακόμα και όταν βγαίνει τέλεια. Αν θέλεις ακέραιο πηλίκο, υπάρχει άλλος τελεστής:pythonprint(7 // 2) print(7 % 2)
3 και 1. Το // δίνει το ακέραιο πηλίκο και το % το υπόλοιπο. Χρειάζονται πιο συχνά από όσο φαντάζεσαι: πόσες πλήρεις συσκευασίες των 6 βγαίνουν από 20 τεμάχια και πόσα περισσεύουν, σε ποια δόση πέφτει ένα ποσό, αν ένας αριθμός είναι ζυγός.Στους αρνητικούς το
// δεν κόβει προς το μηδέν, στρογγυλοποιεί προς τα κάτω: το -7 // 2 δίνει -4, όχι -3.Και μια ευχάριστη διαφορά από τις περισσότερες γλώσσες: ο ακέραιος στην Python δεν έχει ταβάνι.
pythonprint(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. Ρώτα τον τι κρατάει πραγματικά:
pythonprint(format(0.1, ".20f"))
text0.10000000000000000555
Αυτό είναι το «0.1» σου. Δεν είναι το ένα δέκατο, είναι λίγο παραπάνω. Κάνε δύο τρεις πράξεις με τέτοιους αριθμούς και το λάθος βγαίνει στην επιφάνεια.
Το ύπουλο είναι ότι δεν βγαίνει πάντα:
pythonprint(5.35 * 5) print(5.35 * 3)
text26.75 16.049999999999997
Ίδια τιμή, ίδιος πολλαπλασιασμός, δύο εντελώς διαφορετικές εντυπώσεις. Η πρώτη γραμμή σε καθησυχάζει και η δεύτερη σε προδίδει. Δεν είναι ότι οι δεκαδικοί βγαίνουν πάντα λάθος. Είναι ότι δεν ξέρεις πότε, και δεν θα το ελέγχεις κάθε φορά.
Πού κοστίζει
Στην απόδειξη ενός πελάτη δεν κοστίζει τίποτα, γιατί θα τυπώσεις δύο δεκαδικά και το 16.049999999999997 θα γίνει 16.05.
Κοστίζει όταν αθροίζεις. Ένα e-shop με 40.000 παραγγελίες τον μήνα βγάζει ένα σύνολο που δεν συμφωνεί με το άθροισμα των αποδείξεων. Η διαφορά είναι μερικά λεπτά, οπότε κανείς δεν την προσέχει για καιρό, και όταν τελικά τη δουν, κάποιος θα κληθεί να την ψάξει. Κοστίζει επίσης όταν συγκρίνεις: ο έλεγχος «πλήρωσε ακριβώς όσο χρωστούσε» βγαίνει ψευδής για δύο ποσά που στα χαρτιά είναι ίδια.
Και κοστίζει στις στρογγυλοποιήσεις, με τρόπο που δεν τον περιμένεις:
pythonprint(round(2.675, 2))
Τυπώνει
2.67. Όχι επειδή η round είναι χαλασμένη, αλλά επειδή ο αριθμός που της έδωσες ήταν ήδη λίγο κάτω από το 2,675 πριν καν τον δει.Ηroundέχει και μια δεύτερη συμπεριφορά που ξαφνιάζει: τοround(0.5)δίνει0και τοround(2.5)δίνει2. Στα μισά στρογγυλοποιεί προς τον ζυγό, όχι πάντα προς τα πάνω.Είναι σκόπιμο και είναι το πρότυπο που ακολουθεί όλος ο κλάδος, γιατί το «πάντα προς τα πάνω» φουσκώνει συστηματικά τα αθροίσματα.
Για χρήματα υπάρχει σωστό εργαλείο
Η standard library έχει έναν τύπο που κάνει ακριβώς τις πράξεις που θα έκανες με μολύβι, στη βάση 10.
pythonfrom 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) παίρνει τον ήδη χαλασμένο δεκαδικό και τον αντιγράφει με όλη του τη λάσπη:textDecimal('0.1000000000000000055511151231257827021181583404541015625')
Η παγίδα, που είναι στην πραγματικότητα προστασία: η Python αρνείται να ανακατέψει τα δύο είδη.
pythonprint(Decimal("0.10") + 0.20)
textTypeError: 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 και την κοινότητα.