Διάλεξη 24ης Οκτωβρίου 2016

Συναρτήσεις (functions)

Οι συναρτήσεις είναι ο σημαντικότερος μηχανισμός μιας γλώσσας προγραμματισμού ο οποίος επιτρέπει την επαναχρησιμοποίηση κώδικα. Αν γράψουμε μια συνάρτηση τότε μπορούμε να την χρησιμοποιήσουμε, να την "καλέσουμε" όπως λέμε, ξανά και ξανά, χωρις να χρειάζεται να ξαναγράψουμε τις εντολές που υλοποιούν το σκοπό για τον οποίο γράφτηκε. Έχουμε ήδη χρησιμοποιήσει συναρτήσεις: αν s είναι μια ακολουθία χαρακτήρων τότε len(s) είναι το μήκος της ακολουθίας χαρακτήρων s. Λέμε ότι η συνάρτηση len επιστρέφει (returns) το μήκος του ορίσματος (argument) s, το οποίο γράφεται σε παρενθέσεις μετά το όνομα της συνάρτησης.

Έχουμε δει τη χρήση πολλών άλλων συναρτήσεων: η συνάρτηση abs επιστρέφει την απόλυτη τιμή του ορίσματός της και οι συναρτήσεις max και min επιστρέφουν το μέγιστο, αντίστοιχα, ελάχιστο από τα ορίσματά τους:


>>> x = -3.21
>>> print('The absolute value of', x, 'is', abs(x))
The absolute value of -3.21 is 3.21
>>> y = 12.7
>>> z = 1.32
>>> print('The max of', x, y, z, 'is', max(x,y,z))
The max of -3.21 12.7 1.32 is 12.7
>>> print('The min of', x, y, z, 'is', min(x,y,z))
The min of -3.21 12.7 1.32 is -3.21

Οι συναρτήσεις της μαθηματικής βιβλιοθήκες math είναι μερικά ακόμα παραδείγματα συναρτήσεων τις οποίες παρέχει η υλοποίηση της Python στον υπολογιστή που χρησιμοποιούμε. Αυτές είναι οι λεγόμενες ενσωματωμένες συναρτήσεις (built-in functions). Όλες οι γλώσσες προγραμματισμού παρέχουν μηχανισμούς για τη συγγραφή νέων συναρτήσεων και τη χρήση τους σαν να ήταν ενσωματωμένες συναρτήσεις. Στην Python, το συντακτικό του ορισμού μιας συνάρτησης είναι:


def name-of-function (list of formal parameters):
    body of function

Η λέξη def είναι δεσμευμένη λέξη η οποία ανακοινώνει τον ορισμό της συνάρτησης με το όνομα name-of-function. Το όνομα μιας συνάρτησης ακολουθεί τους συνηθισμένους κανόνες της Python: τα ονόματα συναρτήσεων μπορούν να περιέχουν πεζά και κεφαλαία γράμματα, ψηφία (εκτός του πρώτου χαρακτήρα) και τον ειδικό χαρακτήρα '_'. Έτσι, my_function θα μπορούσε να είναι το όνομα μιας συνάρτησης, διαφορετικής όμως της My_Function. Η λίστα των τυπικών παραμέτρων είναι μια ακολουθία ονομάτων χωρισμένα με κόμματα. Για παράδειγμα, θα μπορούσαμε να ορίσουμε τη συνάρτηση avg η οποία υπολογίζει το μέσο όρο των δύο ορισμάτων της x, y ως εξής:


def avg(x, y):
    return (x+y)/2

Μπορούμε να χρησιμοποιήσυμε αυτή τη συνάρτηση όπως και τις ενσωματωμένες συναρτήσεις:


mv = avg(3.12, 1.42)
print('The average of 3.12 and 1.42 is', mv)

Κατά την κλήση της συνάρτησης τα ορίσματά της αντιστοιχίζονται με τη λίστα των τυπικών ορισμάτων όπως ακριβώς και κατά τη διάρκεια μιας εντολής ανάθεσης. Το σώμα της συνάρτησης μπορεί να περιέχει οποιαδήποτε εντολή της Python και διακρίνεται από τις υπόλοιπες εντολές του προγράμματος χρησιμοποιώντας το μηχανισμό της εσοχής (tab) όπως ακριβώς και στις εντολές ελέγχου και τις ανακυκλώσεις. Η ειδική εντολή return, η οποία μπορεί να χρησιμοποιηθεί μόνο μέσα στο σώμα μιας συνάρτησης ορίζει την τιμή την οποία επιστρέφει η συνάρτηση. Στο παράδειγμα παραπάνω, η συνάρτηση avg επιστρέφει την τιμή της έκφρασης (x+y)/2.

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


def sayHello():
    print('Hello!')

για τον ορισμό μιας συνάρτησης η οποία απλά τυπώνει το μήνυμα Hello! όποτε κληθεί:


>>> sayHello()
Hello!

Παρατηρήστε όμως ότι αν είχαμε γράψει print(sayHello(), 'Maria') θα βλέπαμε το μήνυμα None Maria γιατί None είναι η τιμή η οποία επιστρέφει η συνάρτηση sayHello. Δείτε ακόμα ότι η συνάρτηση αυτή δεν έχει τυπικά ορίσματα και γι'αυτό η λίστα των τυπικών ορισμάτων της είναι κενή, αλλά η κλήση της συνάρτηση διατηρεί την κενή λίστα των ορισμάτων.

Ας γράψουμε τώρα μια συνάρτηση η οποία επιστρέφει σε μια ακολουθία χαρακτήρων την ημερομηνία στη μορφή day-month-year ή, εναλλακτικά, στη μορφή month-day-year, όπως συνηθίζεται σε κάποιες χώρες του κόσμου. Είναι λογικό να θεωρήσουμε ότι η λίστα των τυπικών ορισμάτων θα περιλαμβάνει τα day, month, year, καθώς και ένα τέταρτο όρισμα, ας το πούμε reverse το οποίο ορίζει τον τρόπο σχηματισμού της ακολουθίας χαρακτήρων. Αν συνάρτηση κληθεί με τιμή του ορίσματος reverse ίσο με True τότε θα σχηματίσει την ακολουθία χαρακτήρων month-day-year, διαφορετικά την day-month-year.


def dateString(day, month, year, reverse):
    if reverse:
	return str(month) + '-' + str(day) + '-' + str(year)
    else:
        return str(day) + '-' + str(month) + '-' + str(year)

Κατά την κλήση dateString(12, 10, 2016, True) το τυπικό όρισμα day παίρνει την τιμή 12, το όρισμα month την τιμή 10, το όρισμα year την τιμή 2016 και, τέλος, το τυπικό όρισμα reverse την τιμή True. Η αντιστοίχιση λοιπόν τυπικών ορισμάτων και γίνεται σύμφωνα με τη θέση τους. Η Python επιτρέπει και ένα δεύτερο τρόπο αντιστοίχισης τυπικών ορισμάτων και ορισμάτων αναγράφοντας το όνομα του τυπικού ορίσματος, τον τελεστή '=' και την τιμή του τυπικού ορίσματος. Θα μπορούσαμε λοιπόν να γράψουμε dateString(day=12, month=10, year=2016, reverse=True) αλλά και να δώσουμε τιμές στα τυπικά ορίσματα με οποιαδήποτε σειρά θέλουμε:


dateString(year=2016, month=10, day=12, reverse=True)
dateString(day=12, year=2016, month=10, reverse=True)
dateString(reverse=True, day=12, year=2016, month=10)

Δείτε όμως ότι η κλήση dateString(12, month=10, 2016, False) παράγει ένα μήνυμα σφάλματος:


>>> dateString(12, month=10, 2016, False)
SyntaxError: positional argument follows keyword argument

ενώ η κλήση dateString(12, 10, 2016, reverse=False) επιστρέφει την ημερομηνία χωρίς μήνυμα σφάλματος:


>>> dateString(12, 10, 2016, reverse=False)
'12-10-2016'

Ο τελευταίος τρόπος κλήσης μιας συνάρτησης είναι ιδιαίτερα συνηθισμένος σε συνδιασμό με τη χρήση των λεγόμενων προκαθορισμένων τιμών ορισμάτων (default parameter values). Στο παράδειγμα της συνάρτησης dateString θα μπορούσαμε να υποθέσουμε ότι η απουσία του ορίσματος reverse είναι ισοδύναμη με την κλήση της συνάρτησης με το όρισμα reverse να έχει την τιμή False. Μπορούμε να υλοποιήσουμε την ιδέα αυτή ως εξής:


def dateString(day, month, year, reverse=False):
    if reverse:
	return str(month) + '-' + str(day) + '-' + str(year)
    else:
        return str(day) + '-' + str(month) + '-' + str(year)

Έτσι, όλες οι παρακάτω κλήσεις της dateString είναι επιτρεπτές:


dateString(12, 10, 2016)
dateString(12, 10, 2016, True)
dateString(12, 10, 2016, reverse=True)

Η πρώτη θα επιστρέψει την ακολουθία χαρακτήρων '12-10-2016' ενώ οι επόμενες δύο την ακολουθία χαρακτήρων '10-12-2016'.

Ας γράψουμε τώρα μια συνάρτηση η οποία δεδομένου του φυσικού αριθμού $n$ υπολογίζει το άθροισμα $1^2 + 2^2 + \cdots + n^2$.


def sumsq(n):
    if n <= 0:
        return 0
    s = 0
    for i in range(1, n+1):
        s += i*i
    return s

# See if it works

print('Sum of the squares of the first four integers =', sumsq(4))

Εύκολα διαπιστώνει κανείς ότι η κλήση sumsq(4) επιστρέφει τον ακέραιο 30, το σωστό δηλαδή αποτέλεσμα. Όπως είπαμε και νωρίτερα το σώμα μιας συνάρτησης μπορεί να περιέχει οποιαδήποτε εντολή της Python, συμπεριλαμβανομένης, φυσικά, και της εντολής ανάθεσης. Εδώ, το σώμα της συνάρτησης sumsq κάνει χρήση δύο μεταβλητών, των i και s. Η πρώτη είναι η μεταβλητή της ανακύκλωσης for και η δεύτερη περιέχει το τρέχων άθροισμα. Είναι σημαντικό να καταλάβουμε ότι αυτές οι μεταβλητές είναι "τοπικές" με την έννοια ότι είναι άγνωστες στο υπόλοιπο μέρος του προγράμματός μας. Αν, για παράδειγμα, προσπαθούσαμε να τυπώσουμε και την τιμή του μετρητή της ανακύκλωσης, εκτός του σώματος της συνάρτησης, θα παίρναμε το μήνυμα σφάλματος  NameError: name 'i' is not defined. Είναι ακόμα απαραίτητο να πούμε ότι το ίδιο ακριβώς συμβαίνει και με τα τυπικά ορίσματα μιας συνάρτησης: συμπεριφέρονται ως τοπικές μεταβλητές, είναι άγνωστες δηλαδή εκτός του σώματος της συνάρτησης. Το γεγονός αυτό μας επιτρέπει να γράψουμε τον κώδικα για τη συνάρτηση sumsq ως εξής:


def sumsq(n):
    if n <= 0: return 0
    s = 0
    while n > 0:
        s += n*n
        n -= 1
    return s

# See if it works

print('Sum of the squares of the first four integers =', sumsq(4))

Επιπλέον, εκτός του σώματος της συνάρτησης sumsq μπορούμε να χρησιμοποιήσουμε το όνομα n ως όνομα μεταβλητής και η οποία δεν έχει καμμία απολύτως σχέση με το τυπικό όρισμα της sumsq με το ίδιο όνομα. Ίσα-ίσα, αυτό τονίζει τη σχέση με το τοπικό όρισμα της sumsq.


def sumsq(n):
    if n <= 0: return 0
    s = 0
    while n > 0:
        s += n*n
        n -= 1
    return s

# See if it works

n = 7
print('Sum of the squares of the first', n, 'integers =', sumsq(n))