Σημειωματάριο διάλεξης Τρίτης 17 Φεβ. 2015

Εδώ θυμόμαστε πώς χρησιμοποιούμε την ανακύκλωση στην οποία μια μεταβλητή διατρέχει μια λίστα.

In [1]:
ll = [1, 3, 4.3]
for x in ll:
    print x
1
3
4.3

Η ανακύκλωση while

Στο επόμενο ζητάμε ένα ακέραιο αριθμό από το χρήστη και τυπώνουμε όλους τους αριθμούς από το N έως και το 0 με φθίνουσα σειρά.

Η ανακύκλωση που χρησιμοποιούμε είναι ανακύκλωση τύπου while, στην οποία οι εντολές (που υπάγονται σε αυτήν) εκτελούνται εφόσον ισχύει η συνθήκη που είναι στο while.

Εδώ λοιπόν χρησιμοποιούμε τη βοηθητική μεταβλητή i η οποία έχει αρχική τιμή N και η οποία σε κάθε ανακύκλωση (α) τυπώνεται και (β) μειώνεται κατά 1. Η ανακύκλωση σταματάει μόλις το i γίνει αρνητικό.

In [6]:
N = int( raw_input("Give me N:") )
i = N
while i>=0:
    print i,
    i -= 1
Give me N:23
23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

Εδώ βλέπουμε μια περίπτωση στην οποία η χρήση της while και όχι της for είναι σχεδόν υποχρεωτική. Στην ανακύκλωση for η ανακύκλωση εκτελείται τόσες φορές όσες και το μήκος της λίστας την οποία διατρέχουμε. Προϋπόθεση λοιπόν για τη χρήση της for είναι το να μπορούμε, πριν αρχίσει να εκτελείται η ανακύκλωση, να προϋπολογίσουμε το πόσες φορές αυτή θα εκτελεστεί. Αυτή η προϋπόθεση δεν υπάρχει πάντα.

Π.χ., στο παρακάτω πρόγραμμα ο χρήστης δίνει συνεχώς ακεραίους αριθμούς και όταν σταματήσει να δίνει (το οποίο το σηματοδοτεί δίνοντας 0) το πρόγραμμα τυπώνει το άθροισμα όλων των αριθμών που ο χρήστης έδωσε. Ξεκινώντας λοιπόν η όποια ανακύκλωση εδώ δε μπορεί να γνωρίζουμε το πόσους αριθμούς θα δώσει ο χρήστης. Είναι λοιπόν αδύνατο να χρησιμοποιήσουμε την εντολή for για την ανακύκλωσή μας και γι' αυτό το πρόγραμμα το γράφουμε με την while.

Πρώτα τυπώνουμε ένα ενημερωτικό μήνυμα στο χρήστη και θέτουμε τη μεταβλητή s=0. Η μεταβλητή αυτή χρησιμοποιείται για να κρατήσει το τρέχον άθροισμα (το άθροισμα δηλ. των αριθμών που έχει δώσει ο χρήστης μέχρι στιγμής). Έπειτα διαβάζουμε τον πρώτο αριθμό που δίνει η χρήστης με την εντολή x = int( raw_input("Give me a number:") )

Η ανακύκλωση while εκτελείται όσο ο αριθμός x που έδωσε ο χρήστης δεν είναι 0. Αυτό που κάνουμε μέσα στην ανακύκλωση είναι (α) να ενημερώσουμε το τρέχον άθροισμα s αυξάνοντάς το κατά x και (β) να ξαναρωτήσουμε το χρήστη να μας δώσει κι άλλο αριθμό. Αν ο χρήστης δώσει 0 τότε η ανακύκλωση δε ξαναεκτελείται και τυπώνεται το μερικό άθροισμα με την τελευταία γραμμή του προγράμματος. Αν ο χρήστης δώσει κάτι μη μηδενικό τότε η ανακύκλωση ξαναεκτελείται, το μερικό άθροισμα ενημερώνεται εκ νέου, ο χρήστης ερωτάται ξανά, κλπ.

Στην python όταν συναντήσουμε το χαρακτήρα # το υπόλοιπο κομμάτι της γραμμής αγνοείται από τον python interpreter. Είναι σα να μην υπάρχει. Χρησιμοποιούμε αυτό το μηχανισμό για να βάλουμε σχόλια στο πρόγραμμά μας, κείμενο δηλ. που επεξηγεί το πρόγραμμά μας σε άλλους και σε μας, αλλά που δεν εκτελείται και δεν επηρεάζει κατά τα άλλα το πρόγραμμά μας πέρα από το ότι βελτιώνει την αναγνωσιμότητά του. Είναι μια από τις πιο σημαντικές αρχές στον προγραμματισμό (αλλά και από αυτές που αγνοούνται πιο συχνά από όλες) ότι ένα καλό πρόγραμμα πρέπει να έχει και αρκετά σχόλια γραμμένα μέσα του, ώστε να μπορεί να κατανοηθεί και από άλλους αλλά και από τον ίδιο τον προγραμματιστή σε μια μεταγενέστερη ημερομηνία.

Στις γραμμές που είναι σε σχόλια αμέσως πριν το while υπάρχει ένας αρχικός έλεγχος που χριμοποιούνταν (στην αρχική μορφή του προγράμματος) για να δούμε αν ο πρώτος αριθμός που έδινε ο χρήστης ήταν 0 (στην οποία περίπτωση το πρόγραμμα τελείωνε). Κάποιος από το ακροατήριο έκανε την παρατήρηση ότι αυτός ο έλεγχος ήταν περιττός και ότι ο έλεγχος που γινόταν μέσα στη while αρκούσε για να έχουμε το ίδιο αποτέλεσμα. Γι' αυτό μπήκαν οι γραμμές σε σχόλια.

In [8]:
print "Give me numbers. End with 0."
s=0
x = int( raw_input("Give me a number:") )
#if x==0:
#    print 0
#    exit()
while x != 0:
    s += x
    x = int( raw_input("Give me a number:") )
print "The sum is", s
   
Give me numbers. End with 0.
Give me a number:4
Give me a number:5
Give me a number:-3
Give me a number:0
The sum is 6

Πράξεις σε λίστες

Εδώ βλέπουμε διάφορες πράξεις (operations) που μπορεί κανείς να κάνει σε μια λίστα.

Βλέπουμε κατ' αρχήν την πράξη append με την οποία προσθέτουμε ένα στοιχείο στο τέλος της λίστας.

Το ίδιο μπορεί να επιτευχθεί με την πράξη + η οποία, όταν τα ορισματά της είναι λίστες, κολλάει το δεξί της όρισμα στο τέλος του αριστερού του ορίσματος. Έτσι η εντολή l.append(3) μπορεί να επιτευχθεί και μη την l = l + [3] ή ακόμη και με την l += [3]. Η εντολή l = [0] + l λοιπόν τοποθετεί το στοιχείο 0 μπροστά στη λίστα.

Μπορούμε να αναφερθούμε στα στοιχεία της λίστας αν γνωρίζουμε τη θέση τους σε αυτήν. Τα στοιχεία της λίστας είναι τα l[0] (πρώτο στοιχείο), l[1] (δεύτερο στοιχείο), ... , l[len(l)-1] (τελευταίο στοιχείο). Η εντολή λοιπόν l[0] = "xyz" αντικαθιστά το πρώτο στοιχείο της λίστας με το "xyz". Ομοίως η εντολή l[1] = ["a", "b"] αντικαθιστά το δεύτερο στοιχείο της λίστας l με το αντικείμενο ["a", "b"] που είναι και αυτό μια λίστα με δύο στοιχεία (τα strings "a" και "b"). Τέλος η εντολή l[len(l)-1] = 5 αντικαθιστά το τελευταίο στοιχείο της λίστας (αυτό δηλ. με δείκτη len(l)-1) με τον αριθμό 5.

Η εντολή l.pop() διαγράφει από τη λίστα το τελευταίο της στοιχείο ενώ μπορούμε να προσδιορίσουμε το δείκτη του στοιχείου που θέλουμε να διαγράψουμε: η εντολή l.pop(2) διαγράφει από τη λίστα το στοιχείο με δείκτη 2, δηλ. το τρίτο της στοιχείο. Η τελευταία εντολή l[1].pop(1) διαγράφει το δεύτερο στοιχείο του δεύτερου στοιχείου της l που τυγχάνει να είναι και αυτό μια λίστα.

In [20]:
l = [1, 2, "a"]
l.append(3)
print l # τυπώνει: [1, 2, 'a', 3]
l = l + [4, -4]
print l # τυπώνει: [1, 2, 'a', 3, 4, -4]
l = [0] + l
print l # τυπώνει: [0, 1, 2, 'a', 3, 4, -4]
l[0] = "xyz"
print l # τυπώνει: ['xyz', 1, 2, 'a', 3, 4, -4]
l[1] = ["a", "b"]
print l # τυπώνει: ['xyz', ['a', 'b'], 2, 'a', 3, 4, -4]
l[len(l)-1] = 5
print l # τυπώνει: ['xyz', ['a', 'b'], 2, 'a', 3, 4, 5]
l.pop()
print l # τυπώνει: ['xyz', ['a', 'b'], 2, 'a', 3, 4]
l.pop(2)
print l # τυπώνει: ['xyz', ['a', 'b'], 'a', 3, 4]
l[1].pop(1)
print l # τυπώνει: ['xyz', ['a'], 'a', 3, 4]
[1, 2, 'a', 3]
[1, 2, 'a', 3, 4, -4]
[0, 1, 2, 'a', 3, 4, -4]
['xyz', 1, 2, 'a', 3, 4, -4]
['xyz', ['a', 'b'], 2, 'a', 3, 4, -4]
['xyz', ['a', 'b'], 2, 'a', 3, 4, 5]
['xyz', ['a', 'b'], 2, 'a', 3, 4]
['xyz', ['a', 'b'], 'a', 3, 4]
['xyz', ['a'], 'a', 3, 4]

Στο επόμενο κελλί δε βλέπουμε κάτι το ιδιαίτερο αλλά είναι χρήσιμο σε αντιπαραβολή με το μεθεπόμενο.

In [21]:
x = 1
y = x
x = 2
print x, y
2 1

Πράξεις ανάθεσης σε μεταβλητές τύπου λίστας

Στο επόμενο κελλί βλέπουμε κάτι το πολύ σημαντικό, που δείχνει πόσο διαφορετικά αντικείμενα είναι μια αριθμητική μεταβλητή (βλέπε προηγούμενο κελλί) και μια μεταβλητή λίστα.

Το απρόσμενο που βλέπουμε στο κελλί αυτό είναι το εξής: στην τρίτη γραμμή του κώδικα αλλάζουμε το πρώτο στοιχείο της λίστας x, από 'a' σε 'A'. Στην αμέσως προηγούμενη εντολή y=x έχουμε εκχωρήσει (αναθέσει) τη λίστα x στη μεταβλητή x, καθιστώντας έτσι την τιμή της y ίση με ['a', 'b', 'c']. Ασφαλώς δεν περιμένει κανείς η εντολή της τρίτης γραμμής να επηρεάσει την τιμή της y (ακριβώς αυτό γίνεται στο προηγούμενο κελλί με τους αριθμούς, όπου η εκχώρηση x=2 στην τρίτη γραμμή δεν επηρεάζει καθόλου την τιμή της y η οποία αποκτήθηκε στη δεύτερη γραμμή με την εκχώρηση y=x). Όμως με την εκτύπωση print x, y βλέπουμε ότι η y επηρεάστηκε και έχει την ίδια τιμή με τη x.

Τι συνέβη;

Ίσως το καλύτερο που έχουμε να κάνουμε αυτή τη στιγμή είναι να δούμε τη λειτουργία του κώδικα αυτού σε python με τη βοήθεια του Online Python Tutor, που είναι ένα εξαιρετικό εργαλείο που μας βοηθάει να βλέπουμε τις τιμές των διάφορων μεταβλητών σε ένα πρόγραμμα python όπως ο κώδικας εκτελείται. Μπορείτε να δείτε εδώ το πώς ακριβώς μεταβάλλονται οι μεταβλητές x, y όπως εκτελείται ο κώδικας (πατάτε συνεχώς το κουμπί Forward>).

Μια μεταβλητή στην python είναι απλά ένα όνομα. Το όνομα αυτό μπορεί να δείχνει σε ένα αντικείμενο (αριθμό, λίστα, string, κ.ά.). Ο λόγος ύπαρξης των μεταβλητών είναι ακριβώς για να μπορούμε να αναφερόμαστε στα αντιικείμενα αυτά, τα οποία είναι αποθηκευμένα κάπου στη μνήμη του υπολογιστή.

Όταν π.χ. στο πρόγραμμα python που υπάρχει στο αμέσως προηγούμενο κελλί εμφανίζεται για πρώτη φορά η μεταβλητή x και εμφανίζεται σε μια εντολή του τύπου x=1, αυτό που συμβαίνει στο παρασκήνιο είναι ότι (α) δημιουργείται η νέα μεταβλητή x και (β) η μεταβλητή αυτή "δείχνει" σε μια θέση μνήμης όπου είναι αποθηκευμένη η τιμή 1. Αν έπεται η εντολή y=x τότε (α) δημιουργείται η νέα μεταβλητή y (αν δεν έχει χρησιμοποιηθεί πριν και δεν υπάρχει ήδη) και (β) η y δείχνει σε μια θέση μνήμης όπου είναι αποθηκευμένη η τιμή του δεξιού μέλους, δηλ. και πάλι στον αριθμό 1. Όταν μετά εκτελεσθεί η εντολή x=2 αυτό που συμβαίνει είναι ότι η μεταβλητή x δείχνει πλέον σε μια θέση μνήμης όπου είναι αποθηκευμένη η τιμή 2. Αυτή η τελευταία εντολή δεν επηρεάζει την τιμή της y η οποία εξακολουθεί να δείχνει στο 1.

Στο πρόγραμμα python που βρίσκεται στο επόμενο κελλί η μεταβλητή x δημιουργείται και δείχνει σε μια θέση μνήμης όπου είναι αποθηκευμένη η λίστα ['a', 'b', 'c']. Όταν εκτελεστεί η εντολή y=x αυτό που συμβαίνει διαφορετικό με πριν είναι ότι η τιμή της y τίθεται να είναι η τιμή της x, όμως η τιμή της x είναι απλά η θέση μνήμης όπου είναι αποθηκευμένη η λίστα (η διεύθυνση της λίστας όπως λέμε). Αυτό σημαίνει ότι τώρα οι x, y δείχνουν και οι δύο στην ίδια θέση μνήμης. Ότι πράξη λοιπόν κάνουμε από δω και πέρα που επηρεάζει τη λίστα στην οποία δείχνει η x (όπως η x[0] = 'A') θα έχει άμεσο αντίκτυπο και στη λίστα την οποία δείχνει η y, απλούστατα γιατί οι x, y δείχνουν στην ίδια θέση μνήμης. Γι' αυτό και βλέπουμε αυτή τη διαφορετική συμπεριφορά ανάμεσα σε λίστες και αριθμούς.

In [2]:
x = ['a', 'b', 'c']
y = x
x[0] = 'A'
print x, y
['A', 'b', 'c'] ['A', 'b', 'c']

Εδώ, αντίθετα με το προηγούμενο κελλί, βλέπουμε ότι η τροποποίηση της x στην τρίτη γραμμή δεν επηρεάζει την τιμή της y παρά το ότι μετά από την εντολή y=x της γραμμής 2 οι μεταβλητες x, y δείχνουν στην ίδια λίστα στη μνήμη.

Ο λόγος γι' αυτό είναι είναι ότι με την εντολή στη γραμμή 3 δεν τροποποιείται με κανένα τρόπο η λίστα στην οποία δείχνει η x. Απλά δημιουργείται μια νέα λίστα στη μνήμη, η [1, 2, 3], και η μεταβλητή x γυρνάει πλέον και δείχνει σε αυτήν. Βάλτε αυτό το παράδειμα στο Online Python Tutor για να δείτε καθαρά αυτή τη συμπεριφορά.

In [24]:
x = ['a', 'b', 'c']
y = x
x = [1, 2, 3]
print x, y
[1, 2, 3] ['a', 'b', 'c']

Η μέθοδος ταξινόμησης "bubble sort"

Στο επόμενο κελλί υλοποιούμε τον αλγόριθμο bubble sort, ένα αλγόριθμο ταξινόμησης, ο οποίος σκοπό έχει να αναδιατάξει τα περιεχόμενα μιας λίστας αριθμών (της ln στη συγκεκριμένη περίπτωση) κατά αύξουσα σειρά.

Ο αλγόριθμος αυτός δουλεύει ως εξής. Η βασική του πράξη, που εκτελείται πάνω σε ένα ζεύγος θέσεων της λίστας είναι ότι κοιτάει τα περιεχόμενα των θέσεων αυτών και, αν αυτά δεν είναι στη σωστή σειρά, τα εναλλάσσει. Αυτή η πράξη εκτελείται σε διάφορα ζευγάρια θέσεων με αποτέλεσμα στο τέλος αυτών των πράξεων η λίστα να είναι πλέον ταξινομημένη.

Η πράξη αυτή πρώτα εκτελείται σε όλα τα ζεύγη θέσεων που η μια θέση τους είναι η τελευταία της λίστας και η άλλη θέση τους είναι οποιαδήποτε άλλη. Αν πούμε N το μήκος της λίστας (αυτό πετυχαίνουμε με τη δεύτερη γραμμή του προγράμματος) τότε η πρώτη σειρά πράξεων που κάνουμε είναι πάνω στα ζεύγη:

(0, N-1), (1, N-1), (2, N-1), ... , (N-2, N-1)

Είναι φανερό ότι μετά από τις πράξεις αυτές το μεγαλύτερο στοιχείο της λίστας έχει πάει στη θέση του, δηλ. στην τελευταία θέση της λίστας l[N-1].

Μπορούμε συνεπώς να ξεχάσουμε πλέον την τελευταία θέση της λίστας αφού είναι ήδη συμπληρωμένη σωστά και να ξανακάνουμε την ίδια σειρά πράξεων στην υπόλοιπη λίστα l[i], με i=0,1,...,N-2. Κάνουμε λοιπόν μετά τις πράξεις μας πάνω στα ζεύγη

(0,N-2), (1, N-2), ... , (N-3, N-2)

και μετά το τέλος αυτών των πράξεων το δεύτερο μεγαλύτερο στοιχείο της (αρχικής) λίστας έχει πάει στη θέση του, που είναι η προτελευταία l[N-2].

Έπειτα κάνουμε τις πράξεις

(0,N-3), (1, N-3), ... , (N-4, N-3)

οι οποίες ταχτοποιούν το τρίτο μεγαλύτερο στοιχείο της λίστας, κλπ.

Η τελευταία σειρά πράξεων που κάνουμε είναι η

(0,1)

Για να πετύχουμε να παράγουμε τα ζεύγη που φαίνονται παραπάνω με τη σειρά που φαίνονται χρησιμοποιούμε δύο μεταβλητές, την i που παίζει το ρόλο του δεύτερου (μεγαλύτερου) στοιχείου του ζεύγους, και την j που παίζει το ρόλο του πρώτου στοιχείου.

Η μεταβλητή i παίρνει διαδοχικά τιμές από το N-1 έως 1 ενώ για κάθε τιμή της μεταβλητής i η μεταβλητή j παίρνει τιμές από 0 έως i-1. Αυτό το πετυχαίνουμε με το διπλό loop (loop μέσα σε loop) που φτιάχνουμε με τα δύο for στις γραμμές 3 και 4. Το δεύτερο for υπάγεται στο πρώτο και για κάθε τιμή του i εκτελείται ολόκληρο το δεύτερο for loop, με το j δηλ. να παίρνει τις τιμές 0, 1, ... , i-1 οι οποίες είναι οι τιμές που υπάρχουν μέσα στη λίστα range(i). Το εξωτερικό loop (μεταβλητή i) εκτελείται για τις τιμές Ν-1, Ν-2, ... , 3, 2, 1 που είναι οι τιμές που υπάρχουν μέσα στη λίστα range(N-1, 0, -1) (η συνάρτηση range επιδέχεται και αυτό τον πιο πολύπλοκο τρόπο κλήσης (δείτε επεξήγηση εδώ).

Οι εντολές που εκτελούνται μέσα στην εσωτερική ανακύκλωση είναι οι εντολές που εκτελούνται για κάθε ζεύγος τιμών (j,i), ελέγχουμε δηλ. αν τα περιεχόμενα των θέσεων ln[j], ln[i] είναι στη σωστή διάταξη (αυτό το ελέγχουμε με το if) και αν δεν είναι (όταν δηλ. ισχύει ln[j] > ln[i]) εναλλάσσουμε τις τιμές τους με την εντολή

ln[i], ln[j] = ln[j], ln[i]

Με αυτή την εντολή διπλής ανάθεσης αναθέτουμε ταυτόχρονα τιμές και στις δύο μεταβλητές που εμφανίζονται αριστερά. Αυτή είναι μια εντολή που είναι ιδιαιτερότητα της python και δεν απαντάται σε άλλες γλώσσες προγραμματισμού, στις οποίες αυτή η εναλλαγή τιμών θα γίνονταν με τις τρεις εντολές που φαίνονται στα σχόλια αμέσως πάνω από τη διπλη ανάθεση, χρησιμοποιώντας δηλ. μια προσωρινή μεταβλητή t.

In [32]:
ln = [1, 3, 4.3, -1, -2, 0, 5]
N = len(ln)
for i in range(N-1, 0, -1):
    for j in range(i):
        if ln[j] > ln[i]:
            #t = ln[i]
            #ln[i] = ln[j]
            #ln[j] = t
            ln[i], ln[j] = ln[j], ln[i]
print ln
[-2, -1, 0, 1, 3, 4.3, 5]