Σημειωματάριο Τρίτης 21 Απριλίου 2015

Class και object oriented programming

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

Ας ξεκινήσουμε με το παράδειγμα ενός σημείου στις δύο διαστάσεις. Ένα τέτοιο αντικείμενο έχει μια x και μια y συντεταγμένη.

Μπορούμε απλά να ορίσουμε ένα class Point για να έχουμε μεταβλητές οι οποίες περιέχουν μέσα τους όλη την πληροφορία για την αναπαράσταση και επεξεργασία ενός τέτοιου σημείου.

Αυτό επιτυγχάνεται με τον κώδικα

class Point:
    
    def __init__(self, x=0.0, y=0.0):
        self.x = x
        self.y = y

που φαίνεται παρακάτω. Με την εντολή class Point: δηλώνουμε ότι ορίζουμε μια νέα κλάση δεδομένων με το όνομα Point. Στο class Point υπάγεται η συνάρτηση __init__ που φαίνεται αμέσως από κάτω. Το ειδικό όνομα __init__ χρησιμοποιείται για να ορίσουμε τη μέθοδο με την οποία δημιουργείται ένα νέο αντικείμενο της κλάσης (αυτό γίνεται με την εντολή p = Point(3.0, 2) παρακάτω).

Η πρώτη παράμετρος της __init__ με το ειδικό όνομα self αναφέρεται στο αντικείμενο (τύπου class Point) που δημιουργούμε. Την παράμετρο αυτή δεν την περνάμε στη μέθοδο αλλά υπονοείται.

Οι υπόλοιπες παράμετροι (x και y στην περίπτωσή μας) περνιούνται στη μέθοδο και είναι οι συντεταγμένες που θα πάρει το σημείο που δημιουργούμε. Οι συντεταγμένες αυτές αποθηκεύονται στα πεδία (attributes) self.x και self.y, τα οποία έχει μέσα της κάθε μεταβλητή που δημιουργείται ως class Point. Οι προεπιλεγμένες τιμές των x και y είναι 0.0 και αν ο χρήστης δεν τις δώσει (όπως κάνουμε στην εντολή q=Point() παρακάτω μπαίνουν αυτές οι τιμές στη θέση τους.

Στο παρακάτω πρόγραμμα λοιπόν, μετά τον ορισμό της class Point δημιουργούμε ένα Point με συντεταγμένες 3.0, 2 και βάζουμε τη μεταβλητή p να δείχνει σε αυτό. Τυπώνουμε μετά τα πεδία x και y του p, αναφερόμενοι σε αυτά με p.x και p.y. Τέλος δημιουργούμε και ένα class Point q με συνετατγμένες 0,0 και τυπώνουμε και αυτού τις συντεταγμένες του.

In [4]:
class Point:
    
    def __init__(self, x=0.0, y=0.0):
        self.x = x
        self.y = y

p = Point(3.0, 2)

print p.x, p.y

q = Point()
print q.x, q.y
3.0 2
0.0 0.0

Εδώ προσθέτουμε στην class Point δύο μεθόδους, τις show και xreflect.

Με τη μέθοδο show (που έχει ως μόνη παράμετρο το self, και άρα καλείται χωρίς παραμέτρους) απλά τυπώνουμε τα στοιχεία (x και y) του σημείου.

Η μέθοδος xreflect (επίσης καλείται χωρίς παραμέτρους) δημιουργεί (και επιστρέφει) ένα νέο class Point που είναι συμμετρικό του αρχικού ως προς τον άξονα των y. Η δημιουργία αυτού του σημείου γίνειται με κλήση στη συνάρτηση <<κατασκευαστή>> Point στην οποία περνάμε τις κατάλληλες παραμέτρους (το ίδιο x αλλά το αντίθετο y).

Με τις εντολές

p = Point(3.0, 2)
p.show()

Δημιουργούμε το σημείο p και το τυπώνουμε καλώντας τη μέθοδο p.show(), ενώ με τις εντολές

q = p.xreflect()
q.show()

Δημιουργούμε το νέο class Point q καλώντας τη μέθοδο p.xreflect(). Έπειτα δείχνουμε τα στοιχεία του q καλώντας τήν q.show().

In [6]:
class Point:
    
    def __init__(self, x=0.0, y=0.0):
        self.x = x
        self.y = y
        
    def show(self):
        print "Point with coordinates:",self.x,self.y
        
    def xreflect(self):
        return Point(self.x, -self.y)
        

p = Point(3.0, 2)
p.show()

q = p.xreflect()
q.show()
Point with coordinates: 3.0 2
Point with coordinates: 3.0 -2

Εξειδίκευση μιας class

Είναι δυνατό να ορίσουμε μια νέα class ως εξειδίκευση μιας ήδη υπάρχουσας class. Η νέα class κληρονομεί αυτόματα όλα τα δεδομένα και τις μεθόδους της μητρικής εκτός αν η ίδια η νέα class ορίσει νέες μεθόδους με το ίδιο όνομα, οπότε χρησιμοποιούνται αυτές.

Στο παρακάτω παράδειγμα ορίζουμε πρώτα την class Point3D (σημείο στις τρεις διαστάσεις) και ως εξειδίκευση αυτής ορίζουμε μετά την class Point2D. Το ότι η class Point2D έχει ως μητρική την class Point3D φαίνεται στον ορισμό της

class Point2D(Point3D):

Στην class Point3D ορίζουμε την __init__ να παίρνει τρεις παραμέτρους x, y, z με προεπιλεγμένες τιμές 0, και τις αποθηκεύει στα αντίστοιχα πεδία self.x, self.y, self.z. Στην class Point2D δεν ορίζουμε ιδιαίτερη __init__ οπότε χρησιμοποιείται η __init__ της μητρικής. Και στις δύο εντολές

p = Point3D(2, 3, 4)
q = Point2D(1, 2)

που φαίνονται παρακάτω η μέθοδος που καλείται για να κατασκευάσει τα δύο σημεία είναι η __init__ της class Point3D (επειδή οι προεπιλεγμένες τιμές είναι 0 μπορούμε να καλέσουμε την __init__ με δύο παραμέτρους μόνο που θεωρούνται οι x, y ενώ z λαμβάνει την προεπιλεγμένη τιμή της 0.

Και οι δύο class ορίζουν τη δικιά τους μέθοδο show (καλείται χωρίς παραμέτρους) για να τυπώσουν τον εαυτό τους. Έτσι όταν καλούμε p.show() καλείται η show που έχει οριστεί μέσα στην class Point3D ενώ όταν καλούμε q.show() καλείται η __init__ που έχει οριστεί σην εξειδικευμένη class Point2D. Έτσι πετυχαίνουμε όταν τυπώνουμε ένα διδιάστατο σημείο να μη τυπώνεται καθόλου η z συντεταγμένη.

In [9]:
class Point3D:
    
    def __init__(self, x=0.0, y=0.0, z=0.0):
        self.x = x; self.y = y; self.z = z
    
    def show(self):
        print "[{x},{y},{z}]".format(x=self.x, y=self.y,z=self.z)
        
class Point2D(Point3D):
    
    def show(self):
        print "[{x},{y}]".format(x=self.x, y=self.y)
                                     
p = Point3D(2, 3, 4)
q = Point2D(1, 2)

p.show()
q.show()
[2,3,4]
[1,2]

Σε σχέση με το προηγούμενο προσθέτουμε επιπλέον τη μέθοδο myname σε κάθε μια από τις class Point3D και class Point2D (κάνει ότι έκαναν και οι show() προηγουμένως αλλά επιστρέφει το string χωρίς να το τυπώνει.

Η μέθοδος show τώρα έχει υλοποιηθεί στην class Point3D απλά καλώντας τη μέθοδο myname και έχει αφαιρεθεί τελείως από την class Point2D. Ο λόγος είναι ότι με το νέο τρόπο που υλοποιούμε τη show (καλώντας τη myname, η οποία είναι διαφορετική για τη μητρική και τη θυγατρική class) είναι ίδιος για τις δύο κλάσεις οπότε αρκεί η θυγατρική class να χρησιμοποιήσει τη show της μητρικής.

Τέλος έχουμε προσθέσει και μια μέθοδο distance στη μητρική class η οποία υπολογίζει την απόταση από την αρχή των αξόνων (0,0,0).

Στο κυρίς πρόγραμμα που ακολουθεί τους ορισμούς των class:

p = Point3D(2, 3, 4)
q = Point2D(1, 2)

points = []
points.append(p)
points.append(q)

for x in points:
    print "Distance of point {p} is {d}".format(p=x.myname(), d=x.distance())

Ορίζουμε δύο σημεία p, q (το πρώτο είναι class Point3D και το δεύτερο class Point2D) και μια λίστα points που περιέχει μέσα της όλα τα σημεία, διδιάστατα και τρισδιάστατα.

Το σημαντικό εδώ είναι ότι στο τελευταίο loop, όπου διανύουμε αυτή τη λίστα και τυπώνουμε για κάθε σημείο το όνομά του και την απόστασή του από την αρχή των αξόνων, δε χρειαζόμαστε να γνωρίζουμε το τι είδους σημείο είναι το x μια και, όποιο και να είναι, η συνάρτηση x.myname() θα τρέξει με το σωστό τρόπο (η συνάρτηση x.distance() είναι η ίδια και στις δύο class).

Παρατηρούμε εδώ ότι η χρήση του κώδικα που παρέχουν τα δύο class είναι πολύ εύκολη για τον <<τελικό χρήστη>> (αυτόν που γράφει το κυρίως πρόγραμμα που χρησιμοποιεί τα class). Αυτός ακριβώς είναι ο σκοπός: να μεταφέρουμε τον κόπο στον κατασκευαστή της class (ή της όποιας συνάρτησης) από τον χρήστη της class, μια και οι χρήστες είναι εν δυνάμει πάρα πολλοί ενώ ο κατασκευαστής ένας.

In [41]:
import math

class Point3D:
    
    def __init__(self, x=0.0, y=0.0, z=0.0):
        self.x = x; self.y = y; self.z = z
    
    def myname(self):
        return "[{x},{y},{z}]".format(x=self.x, y=self.y,z=self.z)
    
    def show(self):
        print self.myname()
        
    def distance(self):
        return math.sqrt(self.x**2+self.y**2+self.z**2)
        
class Point2D(Point3D):
    
    def myname(self):
        return "[{x},{y}]".format(x=self.x, y=self.y)
                                     
p = Point3D(2, 3, 4)
q = Point2D(1, 2)

points = []
points.append(p)
points.append(q)

for x in points:
    print "Distance of point {p} is {d}".format(p=x.myname(), d=x.distance())
Distance of point [2,3,4] is 5.38516480713
Distance of point [1,2] is 2.2360679775

Κατασκευή κάποιων class για να αναπαραστήσουμε τα άτομα που δουλεύουν σε μια εταιρεία.

Φανταστείτε ότι έχουμε μια εταιρεία που έχει ένα αφεντικό, ο οποίος από κάτω έχει κάποιους managers και ο καθένας από τους managers έχει από κάτω κάποιους εργάτες (workers).

Αρχίζουμε δημιουργώντας μια γενική class Person με σκοπό να την εξειδικεύσουμε σε δύο class την class Manager και την class Worker ώστε κάθε άτομο που δουλεύει στην εταιρεία να είναι σε μια από τις δύο αυτές class (και το αφεντικό το βάζουμε κι αυτό στην class Manager).

Στην class Person βάζουμε όλα τα κοινά χαρακτηριστικά όλων των ατόμων (όνομα, μισθός, τμήμα (dept)). Αυτά δε χρειάζεται να τα ξαναορίσουμε μετά στις εξειδικευμένες class. Υπάρχει επίσης στον ορισμό της class Person και μια μεταβλητή count που παίρνει αρχική τιμή 0 κατά την ώρα ορισμού της class Person και στην οποία μεταβλητή κρατάμε το συνολικό πλήθος των ατόμων που υπάρχουν. Η μεταβλητή αυτή είναι κομμάτι του ορισμού της class (και όχι των αντικειμένων αυτής της class) και άρα αναφερόμαστε σε αυτή ως Person.count.

Η συνάρτηση __init__ για την class Person, με παραμέτρους

__init__(self, name="None", salary=0.0, dept="None")

απλά καλεί τη συνάρτηση

dothisfirst(self, name="None", salary=0.0, dept="None")

με ίδιες ακριβώς παραμέτρους (το κάνουμε έτσι για να μπορούμε να καλούμε την dothisfirst και από τις εξειδικευμένες class). Πέρα από το να αποθηκεύει τις παραμέτρους της στα εσωτερικά πεδία η μέθοδος dothisfirst ενημερώνει και το μετρητή count αυξάνοντάς τον κατά 1.

Στην εξειδικευμένη class Manager έχουμε δύο επιπλέον πεδία: το workers που είναι μια λίστα (αρχικά κενή) από όλους τους εργάτες που είναι υπό τον manager και το boss που είναι το αφεντικό του manager (ή το string "None" αν ο manager για τον οποίο μιλάμε είναι το μεγάλο αφεντικό της εταιρείας).

Η μέθοδος myworkers της class Manager επιστρέφει ένα string με τα ονόματα όλων των εργατών υπό τον manager.

Η μέθοδος mydescription της class Manager επιστρέφει και αυτή ένα string με την περιγραφή του Manager σε μορφή κειμένου. Το πρώτο κομμάτι της μεθόδου αυτής ελέγχει αν στο πεδίο boss βρίσκεται άλλος manager ή το string "None" και φτιάχνει ανάλογα τη μεταβλητή nm που χρησιμοποιείται για το αποτέλεσμα. Η μέθοδος mydescription χρησιμοποιεί τη μέθοδο myworkers για να υπολογίσει το string που επιστρέφει.

Σε σχέση με την class Person η class Worker έχει ένα επιπλέον πεδίο, το manager το οποίο επιπλέον περνιέται ως παράμετρος στην ώρα κατασκευής του αντικειμένου (στη μέθοδο __init__ δηλ.). Η τελευταία εντολή της __init__ της class Worker ενημερώνει το πεδίο workers του αντίστοιχου manager προσθέτοντας στη λίστα αυτή το όνομα τον εργάτη που μόλις δημιουργήθηκε.

Στο class Worker υπάρχει επίσης μια υλοποίηση της μεθόδου mydescription η οποία είναι διαφορετική από αυτή για Manager.

Στο κυρίως πρόγραμμα με τις γραμμές

b = Manager("Eftichis", 3, "all", "None")
q = Manager("Manolis", 2, "math", b)

w = Worker("Mitsos", 1, "math", q)
ww = Worker("Babis", 1, "math", q)

δημιουργούμε το μεγάλο αφεντικό (Eftichis) το manager (Manolis) και δύο εργάτες (Mitsos και Babis). Έπειτα βάζουμε όλα αυτά τα άτομα στη λίστα all και τυπώνουμε το συνολικό αριθμό των ατόμων (μεταβλητή Person.count).

Στο τελευταίο loop

for x in all:
    print x.mydescription()

τυπώνουμε για όλα τα άτομα την περιγραφή τους καλώντας τη μέθοδο mydescription η οποία τυπώνει σε διαφορετική μορφή για workers και σε διαφορετική για managers. Παρατηρείστε και πάλι το πόσο απλό στο να γραφεί είναι αυτό το τελευταίο loop όταν έχει γίνει η δουλειά υποδομής στο γράψιμο των class. Παρατηρείστε ακόμη ότι το τελευταίο αυτό loop δε θα άλλαζε στο παραμικρό αν στο μέλλον αποφασίζαμε να ορίσουμε μια καινούργια εξειδίκευση είτε του class Person είτε μιας από τις κλάσεις class Manager ή class Worker. Μπορεί για παράδειγμα να αποφασίσουμε ότι θα ορίσουμε μια νέα class Consulatant για να χειριστούμε μια κατηγορία συνεργατών της επιχείρησης (σύμβουλοι) οι οποίοι όμως δεν εντάσσονται κάτω από τους managers αλλά κατευθείαν κάτω από το μεγάλο αφεντικό. Σε αυτή την περίπτωση θα πρέπει να φροντίσουμε για αυτή τη νέα class να γράψουμε τις κατάλληλες μεθόδους __init__ και mydescription τουλάχιστον. Το τελικό loop του προγράμματος δουλεύει όπως είναι.

In [3]:
class Person:
    
    count = 0
    
    def dothisfirst(self, name="None", salary=0.0, dept="None"):
        self.name = name
        self.salary = salary+0.0
        self.dept = dept
        Person.count += 1
    
    def __init__(self, name="None", salary=0.0, dept="None"):
        self.dothisfirst(name, salary, dept)
        
class Manager(Person):
    
    def __init__(self, name="None", salary=0.0, dept="None", boss="None"):
        self.dothisfirst(name, salary, dept)
        self.workers = []
        self.boss = boss
    
    def myworkers(self):
        s=""
        if len(self.workers)==0:
            return s
        s = self.workers[0].name
        for x in self.workers[1:]:
            s = s+","+x.name
        return s

    def mydescription(self):
        if isinstance(self.boss, Person):
            nm = self.boss.name
        else:
            nm = self.boss
        s = ( "{onoma}, dept={dept}, salary={salary}, boss={boss}, workers={w}".
             format(onoma=self.name, dept=self.dept, salary=self.salary, boss=nm, w=self.myworkers()) )
        return s
    
class Worker(Person):
    
    def __init__(self, name="None", salary=0.0, dept="None", manager="None"):
        self.dothisfirst(name, salary, dept)
        self.manager = manager
        manager.workers.append(self)
        
    def mydescription(self):
        s = ( "{o}, dept={d}, salary={s}, manager={m}".
             format(o=self.name, d=self.dept, s=self.salary, m=self.manager.name) )
        return s
    
b = Manager("Eftichis", 3, "all", "None")
q = Manager("Manolis", 2, "math", b)

w = Worker("Mitsos", 1, "math", q)
ww = Worker("Babis", 1, "math", q)

all = [b, q, w, ww]

print "Number of persons:", Person.count

for x in all:
    print x.mydescription()
Number of persons: 4
Eftichis, dept=all, salary=3.0, boss=None, workers=
Manolis, dept=math, salary=2.0, boss=Eftichis, workers=Mitsos,Babis
Mitsos, dept=math, salary=1.0, manager=Manolis
Babis, dept=math, salary=1.0, manager=Manolis